| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Algorithm::Classifier::IsolationForest; |
|
2
|
|
|
|
|
|
|
|
|
3
|
42
|
|
|
42
|
|
2268776
|
use strict; |
|
|
42
|
|
|
|
|
70
|
|
|
|
42
|
|
|
|
|
1294
|
|
|
4
|
42
|
|
|
42
|
|
159
|
use warnings; |
|
|
42
|
|
|
|
|
82
|
|
|
|
42
|
|
|
|
|
1843
|
|
|
5
|
42
|
|
|
42
|
|
188
|
use Carp qw(croak); |
|
|
42
|
|
|
|
|
73
|
|
|
|
42
|
|
|
|
|
2121
|
|
|
6
|
42
|
|
|
42
|
|
253
|
use List::Util qw(min); |
|
|
42
|
|
|
|
|
140
|
|
|
|
42
|
|
|
|
|
3597
|
|
|
7
|
42
|
|
|
42
|
|
17631
|
use POSIX qw(ceil); |
|
|
42
|
|
|
|
|
254995
|
|
|
|
42
|
|
|
|
|
247
|
|
|
8
|
42
|
|
|
42
|
|
69676
|
use JSON::PP (); |
|
|
42
|
|
|
|
|
391132
|
|
|
|
42
|
|
|
|
|
1117
|
|
|
9
|
42
|
|
|
42
|
|
19447
|
use File::Slurp qw(read_file write_file); |
|
|
42
|
|
|
|
|
749884
|
|
|
|
42
|
|
|
|
|
3230
|
|
|
10
|
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
our $VERSION = '0.4.0'; |
|
12
|
|
|
|
|
|
|
|
|
13
|
42
|
|
|
42
|
|
323
|
use constant EULER => 0.5772156649015329; |
|
|
42
|
|
|
|
|
98
|
|
|
|
42
|
|
|
|
|
2172
|
|
|
14
|
42
|
|
|
42
|
|
189
|
use constant TWO_PI => 6.283185307179586; |
|
|
42
|
|
|
|
|
95
|
|
|
|
42
|
|
|
|
|
1385
|
|
|
15
|
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
# Node-type tags stored in index 0 of every tree node arrayref. |
|
17
|
|
|
|
|
|
|
# 0 is falsy, so while ($node->[0]) acts as while (!leaf). |
|
18
|
42
|
|
|
42
|
|
181
|
use constant _NODE_LEAF => 0; |
|
|
42
|
|
|
|
|
64
|
|
|
|
42
|
|
|
|
|
1194
|
|
|
19
|
42
|
|
|
42
|
|
132
|
use constant _NODE_AXIS => 1; |
|
|
42
|
|
|
|
|
55
|
|
|
|
42
|
|
|
|
|
1285
|
|
|
20
|
42
|
|
|
42
|
|
150
|
use constant _NODE_OBLIQUE => 2; |
|
|
42
|
|
|
|
|
59
|
|
|
|
42
|
|
|
|
|
357500
|
|
|
21
|
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
23
|
|
|
|
|
|
|
# Optional Inline::C accelerator for the scoring hot path. |
|
24
|
|
|
|
|
|
|
# |
|
25
|
|
|
|
|
|
|
# pack_input_xs(data_sv, out_sv, n_pts, n_feats, miss_mode, fill_sv) |
|
26
|
|
|
|
|
|
|
# Walks the Perl arrayref-of-arrayrefs and writes a packed double buffer |
|
27
|
|
|
|
|
|
|
# into out_sv. Replaces the dominant per-call Perl map-pack loop. |
|
28
|
|
|
|
|
|
|
# miss_mode selects how an undef cell is packed: 0 => 0.0, 1 => the |
|
29
|
|
|
|
|
|
|
# per-feature fill from fill_sv (impute), 2 => NaN (nan strategy). |
|
30
|
|
|
|
|
|
|
# |
|
31
|
|
|
|
|
|
|
# score_all_xs(nodes_av, idx_av, val_av, x_sv, sm_sv, |
|
32
|
|
|
|
|
|
|
# n_pts, n_feats, n_trees, use_openmp) |
|
33
|
|
|
|
|
|
|
# Sums path lengths for all n_pts query points across all n_trees trees |
|
34
|
|
|
|
|
|
|
# in one call. Outer loop over points is OpenMP-parallel when the |
|
35
|
|
|
|
|
|
|
# module was built with OpenMP (each iteration writes to a unique sm[i], |
|
36
|
|
|
|
|
|
|
# so no synchronisation is needed). Tree pointers are extracted from |
|
37
|
|
|
|
|
|
|
# the AVs before the parallel region; the parallel region touches only |
|
38
|
|
|
|
|
|
|
# raw int / double buffers. |
|
39
|
|
|
|
|
|
|
# |
|
40
|
|
|
|
|
|
|
# Node layout (6 doubles per node, "IF_NZ = 6"): |
|
41
|
|
|
|
|
|
|
# leaf: [0, size, c(size), 0, 0, 0] |
|
42
|
|
|
|
|
|
|
# axis: [1, attr, split, li, ri, 0] |
|
43
|
|
|
|
|
|
|
# oblique: [2, coff, nf, li, ri, b] |
|
44
|
|
|
|
|
|
|
# |
|
45
|
|
|
|
|
|
|
# c(size) is the expected-path-length adjustment for a leaf holding |
|
46
|
|
|
|
|
|
|
# `size` points, precomputed by _pack_tree (it involves a log(); doing |
|
47
|
|
|
|
|
|
|
# it at pack time keeps transcendentals out of the per-point per-tree |
|
48
|
|
|
|
|
|
|
# scoring loop). The fit-time TreeBuf writer leaves that slot 0 -- |
|
49
|
|
|
|
|
|
|
# its buffers are unpacked into Perl trees and re-packed by |
|
50
|
|
|
|
|
|
|
# _pack_tree before score_all_xs ever sees them. |
|
51
|
|
|
|
|
|
|
# |
|
52
|
|
|
|
|
|
|
# Coefficient storage uses a Structure-of-Arrays layout: one int32 array |
|
53
|
|
|
|
|
|
|
# per tree (feature indices, packed with 'l*') and one double array per |
|
54
|
|
|
|
|
|
|
# tree (coefficients, packed with 'd*'). Both are indexed by `coff` -- |
|
55
|
|
|
|
|
|
|
# the same offset addresses paired entries in the two arrays. Splitting |
|
56
|
|
|
|
|
|
|
# them this way halves index bandwidth, removes the per-element |
|
57
|
|
|
|
|
|
|
# (int) cast inside the SIMD loop, and lets the value loads be |
|
58
|
|
|
|
|
|
|
# contiguous so the compiler emits a clean FMA chain over val[k] with |
|
59
|
|
|
|
|
|
|
# the feature gather on xi[idx[k]] kept separate. |
|
60
|
|
|
|
|
|
|
# |
|
61
|
|
|
|
|
|
|
# Dense-pack fast path: when an oblique node uses every feature (the |
|
62
|
|
|
|
|
|
|
# common case in extended mode with extension_level == n_features - 1), |
|
63
|
|
|
|
|
|
|
# _pack_tree writes its coefficients in feature order so val[k] is the |
|
64
|
|
|
|
|
|
|
# coefficient for feature k. score_all_xs detects this via `nf == |
|
65
|
|
|
|
|
|
|
# n_feats` and uses a no-gather dot product (dot += val[k] * xi[k]) |
|
66
|
|
|
|
|
|
|
# that vectorizes cleanly with FMA -- substantially faster than the |
|
67
|
|
|
|
|
|
|
# sparse gather path on high-feature-count models. |
|
68
|
|
|
|
|
|
|
# x: row-major doubles, n_pts rows of n_feats each. |
|
69
|
|
|
|
|
|
|
# sums: out double array of length n_pts; score_all_xs writes once per i. |
|
70
|
|
|
|
|
|
|
# |
|
71
|
|
|
|
|
|
|
# OpenMP is enabled at module load when the toolchain accepts -fopenmp and |
|
72
|
|
|
|
|
|
|
# libgomp is linkable; otherwise the same C code compiles to a serial loop |
|
73
|
|
|
|
|
|
|
# (the #pragma is silently ignored without _OPENMP defined). |
|
74
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
75
|
|
|
|
|
|
|
our $HAS_C = 0; |
|
76
|
|
|
|
|
|
|
our $HAS_OPENMP = 0; |
|
77
|
|
|
|
|
|
|
our $HAS_SIMD = 0; |
|
78
|
|
|
|
|
|
|
our $OPT_LEVEL = ''; # the actual -O.../-march=... flags used to build, if any |
|
79
|
|
|
|
|
|
|
our $C_SOURCE = ''; # 'prebuilt' (object installed at `make` time) or |
|
80
|
|
|
|
|
|
|
# 'runtime' (compiled at first load into _Inline/); |
|
81
|
|
|
|
|
|
|
# '' when $HAS_C is 0 |
|
82
|
|
|
|
|
|
|
{ |
|
83
|
|
|
|
|
|
|
my $C_CODE = <<'__INLINE_C__'; |
|
84
|
|
|
|
|
|
|
#include |
|
85
|
|
|
|
|
|
|
#include |
|
86
|
|
|
|
|
|
|
#include |
|
87
|
|
|
|
|
|
|
#ifdef _OPENMP |
|
88
|
|
|
|
|
|
|
#include |
|
89
|
|
|
|
|
|
|
#endif |
|
90
|
|
|
|
|
|
|
#define IF_NZ 6 |
|
91
|
|
|
|
|
|
|
|
|
92
|
|
|
|
|
|
|
/* Data prefetch hint; a no-op on compilers without __builtin_prefetch. |
|
93
|
|
|
|
|
|
|
* Purely a performance hint -- never affects results. */ |
|
94
|
|
|
|
|
|
|
#if defined(__GNUC__) || defined(__clang__) |
|
95
|
|
|
|
|
|
|
#define IF_PREFETCH(p) __builtin_prefetch(p) |
|
96
|
|
|
|
|
|
|
#else |
|
97
|
|
|
|
|
|
|
#define IF_PREFETCH(p) |
|
98
|
|
|
|
|
|
|
#endif |
|
99
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
int has_openmp_xs(){ |
|
101
|
|
|
|
|
|
|
#ifdef _OPENMP |
|
102
|
|
|
|
|
|
|
return 1; |
|
103
|
|
|
|
|
|
|
#else |
|
104
|
|
|
|
|
|
|
return 0; |
|
105
|
|
|
|
|
|
|
#endif |
|
106
|
|
|
|
|
|
|
} |
|
107
|
|
|
|
|
|
|
|
|
108
|
|
|
|
|
|
|
/* SIMD on the extended-mode oblique dot product is enabled via |
|
109
|
|
|
|
|
|
|
* `#pragma omp simd`, which OpenMP 4.0 (_OPENMP == 201307) introduced. |
|
110
|
|
|
|
|
|
|
* Anything older silently ignores the pragma -- the loop still runs, |
|
111
|
|
|
|
|
|
|
* just not auto-vectorised. So "simd available" really means the |
|
112
|
|
|
|
|
|
|
* compiler is going to honour the pragma we put on that loop. */ |
|
113
|
|
|
|
|
|
|
int has_simd_xs(){ |
|
114
|
|
|
|
|
|
|
#if defined(_OPENMP) && _OPENMP >= 201307 |
|
115
|
|
|
|
|
|
|
return 1; |
|
116
|
|
|
|
|
|
|
#else |
|
117
|
|
|
|
|
|
|
return 0; |
|
118
|
|
|
|
|
|
|
#endif |
|
119
|
|
|
|
|
|
|
} |
|
120
|
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
/* pack_input_xs(data_sv, out_sv, n_pts, n_feats, miss_mode, fill_sv) |
|
122
|
|
|
|
|
|
|
* |
|
123
|
|
|
|
|
|
|
* Walks a Perl arrayref-of-arrayrefs (n_pts rows of n_feats doubles each) |
|
124
|
|
|
|
|
|
|
* directly in C and writes the packed double buffer into out_sv (which the |
|
125
|
|
|
|
|
|
|
* caller pre-allocates with "\0" x (n_pts*n_feats*8)). Replaces |
|
126
|
|
|
|
|
|
|
* |
|
127
|
|
|
|
|
|
|
* pack('d*', map { my $r=$_; map { $r->[$_] // 0 } 0..$nf-1 } @$data) |
|
128
|
|
|
|
|
|
|
* |
|
129
|
|
|
|
|
|
|
* which was the dominant per-call overhead for high feature counts. |
|
130
|
|
|
|
|
|
|
* |
|
131
|
|
|
|
|
|
|
* miss_mode selects what an undef cell (or missing row) becomes: |
|
132
|
|
|
|
|
|
|
* 0 => 0.0 (the 'die'/'zero' missing strategies) |
|
133
|
|
|
|
|
|
|
* 1 => fill[k] (the 'impute' strategy; fill_sv is a packed |
|
134
|
|
|
|
|
|
|
* double buffer of n_feats per-feature fill values) |
|
135
|
|
|
|
|
|
|
* 2 => NaN (the 'nan' strategy; the C scorer's `<` / `<=` |
|
136
|
|
|
|
|
|
|
* comparisons are both false for NaN, so a point |
|
137
|
|
|
|
|
|
|
* missing the split feature falls to the right |
|
138
|
|
|
|
|
|
|
* child -- matching how fit() routes it) |
|
139
|
|
|
|
|
|
|
* fill_sv is only dereferenced when miss_mode == 1. */ |
|
140
|
|
|
|
|
|
|
void pack_input_xs(SV* data_sv, SV* out_sv, int n_pts, int n_feats, |
|
141
|
|
|
|
|
|
|
int miss_mode, SV* fill_sv){ |
|
142
|
|
|
|
|
|
|
STRLEN tl; |
|
143
|
|
|
|
|
|
|
double* out; |
|
144
|
|
|
|
|
|
|
const double* fill = NULL; |
|
145
|
|
|
|
|
|
|
double missval; |
|
146
|
|
|
|
|
|
|
AV* outer; |
|
147
|
|
|
|
|
|
|
int i, k; |
|
148
|
|
|
|
|
|
|
|
|
149
|
|
|
|
|
|
|
if (!SvROK(data_sv) || SvTYPE(SvRV(data_sv)) != SVt_PVAV) { |
|
150
|
|
|
|
|
|
|
croak("pack_input_xs: data must be an arrayref"); |
|
151
|
|
|
|
|
|
|
} |
|
152
|
|
|
|
|
|
|
outer = (AV*)SvRV(data_sv); |
|
153
|
|
|
|
|
|
|
out = (double*)SvPVbyte_force(out_sv, tl); |
|
154
|
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
if (miss_mode == 1) { |
|
156
|
|
|
|
|
|
|
STRLEN fl; |
|
157
|
|
|
|
|
|
|
fill = (const double*)SvPVbyte(fill_sv, fl); |
|
158
|
|
|
|
|
|
|
} |
|
159
|
|
|
|
|
|
|
missval = (miss_mode == 2) ? NAN : 0.0; |
|
160
|
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
for (i = 0; i < n_pts; i++) { |
|
162
|
|
|
|
|
|
|
SV** row_pp = av_fetch(outer, i, 0); |
|
163
|
|
|
|
|
|
|
double* dst = out + (size_t)i * (size_t)n_feats; |
|
164
|
|
|
|
|
|
|
if (!row_pp || !*row_pp || !SvROK(*row_pp) || |
|
165
|
|
|
|
|
|
|
SvTYPE(SvRV(*row_pp)) != SVt_PVAV) { |
|
166
|
|
|
|
|
|
|
for (k = 0; k < n_feats; k++) |
|
167
|
|
|
|
|
|
|
dst[k] = (miss_mode == 1) ? fill[k] : missval; |
|
168
|
|
|
|
|
|
|
continue; |
|
169
|
|
|
|
|
|
|
} |
|
170
|
|
|
|
|
|
|
{ |
|
171
|
|
|
|
|
|
|
AV* row = (AV*)SvRV(*row_pp); |
|
172
|
|
|
|
|
|
|
for (k = 0; k < n_feats; k++) { |
|
173
|
|
|
|
|
|
|
SV** v = av_fetch(row, k, 0); |
|
174
|
|
|
|
|
|
|
if (v && *v && SvOK(*v)) { |
|
175
|
|
|
|
|
|
|
dst[k] = SvNV(*v); |
|
176
|
|
|
|
|
|
|
} else { |
|
177
|
|
|
|
|
|
|
dst[k] = (miss_mode == 1) ? fill[k] : missval; |
|
178
|
|
|
|
|
|
|
} |
|
179
|
|
|
|
|
|
|
} |
|
180
|
|
|
|
|
|
|
} |
|
181
|
|
|
|
|
|
|
} |
|
182
|
|
|
|
|
|
|
} |
|
183
|
|
|
|
|
|
|
|
|
184
|
|
|
|
|
|
|
/* finalize_scores_xs(sm_sv, n_pts, inv, out_rv) |
|
185
|
|
|
|
|
|
|
* |
|
186
|
|
|
|
|
|
|
* Fills the pre-allocated arrayref out_rv with exp(-sm[i] * inv) for |
|
187
|
|
|
|
|
|
|
* i in 0..n_pts-1. Replaces the trailing |
|
188
|
|
|
|
|
|
|
* |
|
189
|
|
|
|
|
|
|
* my @sums = unpack('d*', $sums_packed); |
|
190
|
|
|
|
|
|
|
* return [ map { exp(-$_ * $inv) } @sums ]; |
|
191
|
|
|
|
|
|
|
* |
|
192
|
|
|
|
|
|
|
* which allocated ~2*n_pts intermediate Perl SVs per scoring call. */ |
|
193
|
|
|
|
|
|
|
void finalize_scores_xs(SV* sm_sv, int n_pts, double inv, SV* out_rv){ |
|
194
|
|
|
|
|
|
|
STRLEN tl; |
|
195
|
|
|
|
|
|
|
const double* sm; |
|
196
|
|
|
|
|
|
|
AV* out; |
|
197
|
|
|
|
|
|
|
int i; |
|
198
|
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
if (!SvROK(out_rv) || SvTYPE(SvRV(out_rv)) != SVt_PVAV) { |
|
200
|
|
|
|
|
|
|
croak("finalize_scores_xs: out must be an arrayref"); |
|
201
|
|
|
|
|
|
|
} |
|
202
|
|
|
|
|
|
|
sm = (const double*)SvPVbyte(sm_sv, tl); |
|
203
|
|
|
|
|
|
|
out = (AV*)SvRV(out_rv); |
|
204
|
|
|
|
|
|
|
av_clear(out); |
|
205
|
|
|
|
|
|
|
if (n_pts > 0) av_extend(out, n_pts - 1); |
|
206
|
|
|
|
|
|
|
for (i = 0; i < n_pts; i++) { |
|
207
|
|
|
|
|
|
|
av_store(out, i, newSVnv(exp(-sm[i] * inv))); |
|
208
|
|
|
|
|
|
|
} |
|
209
|
|
|
|
|
|
|
} |
|
210
|
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
/* finalize_path_lengths_xs(sm_sv, n_pts, t, out_rv) |
|
212
|
|
|
|
|
|
|
* |
|
213
|
|
|
|
|
|
|
* Same idea as finalize_scores_xs but writes sm[i] / t (the average path |
|
214
|
|
|
|
|
|
|
* length across n_trees=t trees) instead of the exp normalisation. */ |
|
215
|
|
|
|
|
|
|
void finalize_path_lengths_xs(SV* sm_sv, int n_pts, double t, SV* out_rv){ |
|
216
|
|
|
|
|
|
|
STRLEN tl; |
|
217
|
|
|
|
|
|
|
const double* sm; |
|
218
|
|
|
|
|
|
|
AV* out; |
|
219
|
|
|
|
|
|
|
int i; |
|
220
|
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
if (!SvROK(out_rv) || SvTYPE(SvRV(out_rv)) != SVt_PVAV) { |
|
222
|
|
|
|
|
|
|
croak("finalize_path_lengths_xs: out must be an arrayref"); |
|
223
|
|
|
|
|
|
|
} |
|
224
|
|
|
|
|
|
|
sm = (const double*)SvPVbyte(sm_sv, tl); |
|
225
|
|
|
|
|
|
|
out = (AV*)SvRV(out_rv); |
|
226
|
|
|
|
|
|
|
av_clear(out); |
|
227
|
|
|
|
|
|
|
if (n_pts > 0) av_extend(out, n_pts - 1); |
|
228
|
|
|
|
|
|
|
for (i = 0; i < n_pts; i++) { |
|
229
|
|
|
|
|
|
|
av_store(out, i, newSVnv(sm[i] / t)); |
|
230
|
|
|
|
|
|
|
} |
|
231
|
|
|
|
|
|
|
} |
|
232
|
|
|
|
|
|
|
|
|
233
|
|
|
|
|
|
|
/* predict_sums_xs(sm_sv, n_pts, sum_threshold, out_rv) |
|
234
|
|
|
|
|
|
|
* |
|
235
|
|
|
|
|
|
|
* Fills out_rv with 0/1 IVs based on sm[i] <= sum_threshold. The caller |
|
236
|
|
|
|
|
|
|
* pre-computes sum_threshold = -log(score_threshold) * c * n_trees / log(2), |
|
237
|
|
|
|
|
|
|
* so this skips both the per-point exp() and the intermediate scores |
|
238
|
|
|
|
|
|
|
* arrayref that the old "score_samples + map threshold" path created. */ |
|
239
|
|
|
|
|
|
|
void predict_sums_xs(SV* sm_sv, int n_pts, double sum_threshold, SV* out_rv){ |
|
240
|
|
|
|
|
|
|
STRLEN tl; |
|
241
|
|
|
|
|
|
|
const double* sm; |
|
242
|
|
|
|
|
|
|
AV* out; |
|
243
|
|
|
|
|
|
|
int i; |
|
244
|
|
|
|
|
|
|
|
|
245
|
|
|
|
|
|
|
if (!SvROK(out_rv) || SvTYPE(SvRV(out_rv)) != SVt_PVAV) { |
|
246
|
|
|
|
|
|
|
croak("predict_sums_xs: out must be an arrayref"); |
|
247
|
|
|
|
|
|
|
} |
|
248
|
|
|
|
|
|
|
sm = (const double*)SvPVbyte(sm_sv, tl); |
|
249
|
|
|
|
|
|
|
out = (AV*)SvRV(out_rv); |
|
250
|
|
|
|
|
|
|
av_clear(out); |
|
251
|
|
|
|
|
|
|
if (n_pts > 0) av_extend(out, n_pts - 1); |
|
252
|
|
|
|
|
|
|
for (i = 0; i < n_pts; i++) { |
|
253
|
|
|
|
|
|
|
av_store(out, i, newSViv(sm[i] <= sum_threshold ? 1 : 0)); |
|
254
|
|
|
|
|
|
|
} |
|
255
|
|
|
|
|
|
|
} |
|
256
|
|
|
|
|
|
|
|
|
257
|
|
|
|
|
|
|
/* score_predict_xs(sm_sv, n_pts, inv, sum_threshold, out_rv) |
|
258
|
|
|
|
|
|
|
* |
|
259
|
|
|
|
|
|
|
* Combines finalize_scores_xs + predict_sums_xs: fills the pre-allocated |
|
260
|
|
|
|
|
|
|
* out_rv with [score, label] pairs in one pass over sm_sv. Replaces the |
|
261
|
|
|
|
|
|
|
* trailing Perl loop in score_predict_samples that built ~3*n_pts SVs |
|
262
|
|
|
|
|
|
|
* (n_pts scores + n_pts labels + n_pts inner arrayrefs) via a Perl |
|
263
|
|
|
|
|
|
|
* foreach -- here the same SVs are allocated directly inside C. |
|
264
|
|
|
|
|
|
|
* |
|
265
|
|
|
|
|
|
|
* Refcount note: newRV_noinc takes ownership of the inner AV without |
|
266
|
|
|
|
|
|
|
* incrementing it, and av_store takes ownership of the RV. When the |
|
267
|
|
|
|
|
|
|
* outer AV is destroyed it frees the RVs, which free the inner AVs, |
|
268
|
|
|
|
|
|
|
* which free the score/label SVs. No leak. */ |
|
269
|
|
|
|
|
|
|
void score_predict_xs(SV* sm_sv, int n_pts, double inv, |
|
270
|
|
|
|
|
|
|
double sum_threshold, SV* out_rv){ |
|
271
|
|
|
|
|
|
|
STRLEN tl; |
|
272
|
|
|
|
|
|
|
const double* sm; |
|
273
|
|
|
|
|
|
|
AV* out; |
|
274
|
|
|
|
|
|
|
int i; |
|
275
|
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
if (!SvROK(out_rv) || SvTYPE(SvRV(out_rv)) != SVt_PVAV) { |
|
277
|
|
|
|
|
|
|
croak("score_predict_xs: out must be an arrayref"); |
|
278
|
|
|
|
|
|
|
} |
|
279
|
|
|
|
|
|
|
sm = (const double*)SvPVbyte(sm_sv, tl); |
|
280
|
|
|
|
|
|
|
out = (AV*)SvRV(out_rv); |
|
281
|
|
|
|
|
|
|
av_clear(out); |
|
282
|
|
|
|
|
|
|
if (n_pts > 0) av_extend(out, n_pts - 1); |
|
283
|
|
|
|
|
|
|
for (i = 0; i < n_pts; i++) { |
|
284
|
|
|
|
|
|
|
AV* row = newAV(); |
|
285
|
|
|
|
|
|
|
av_extend(row, 1); |
|
286
|
|
|
|
|
|
|
/* av_extend filled both slots with &PL_sv_undef. Since that |
|
287
|
|
|
|
|
|
|
* sentinel is immortal (its refcount is never freed) we can |
|
288
|
|
|
|
|
|
|
* overwrite the slots directly and bump AvFILLp, skipping the |
|
289
|
|
|
|
|
|
|
* per-element bounds/magic checks av_store would do. */ |
|
290
|
|
|
|
|
|
|
AvARRAY(row)[0] = newSVnv(exp(-sm[i] * inv)); |
|
291
|
|
|
|
|
|
|
AvARRAY(row)[1] = newSViv(sm[i] <= sum_threshold ? 1 : 0); |
|
292
|
|
|
|
|
|
|
AvFILLp(row) = 1; |
|
293
|
|
|
|
|
|
|
av_store(out, i, newRV_noinc((SV*)row)); |
|
294
|
|
|
|
|
|
|
} |
|
295
|
|
|
|
|
|
|
} |
|
296
|
|
|
|
|
|
|
|
|
297
|
|
|
|
|
|
|
/* score_predict_split_xs(sm_sv, n_pts, inv, sum_threshold, |
|
298
|
|
|
|
|
|
|
* scores_rv, labels_rv) |
|
299
|
|
|
|
|
|
|
* |
|
300
|
|
|
|
|
|
|
* Parallel-arrays variant of score_predict_xs: fills two pre-allocated |
|
301
|
|
|
|
|
|
|
* arrayrefs (scores: NV, labels: IV) instead of an AV-of-[score, label] |
|
302
|
|
|
|
|
|
|
* pairs. Allocates ~2*n_pts SVs instead of ~4*n_pts -- no inner AV and |
|
303
|
|
|
|
|
|
|
* no RV per point -- so it's about twice as cheap for callers that |
|
304
|
|
|
|
|
|
|
* don't need the paired shape. */ |
|
305
|
|
|
|
|
|
|
void score_predict_split_xs(SV* sm_sv, int n_pts, double inv, |
|
306
|
|
|
|
|
|
|
double sum_threshold, |
|
307
|
|
|
|
|
|
|
SV* scores_rv, SV* labels_rv){ |
|
308
|
|
|
|
|
|
|
STRLEN tl; |
|
309
|
|
|
|
|
|
|
const double* sm; |
|
310
|
|
|
|
|
|
|
AV* scores; |
|
311
|
|
|
|
|
|
|
AV* labels; |
|
312
|
|
|
|
|
|
|
int i; |
|
313
|
|
|
|
|
|
|
|
|
314
|
|
|
|
|
|
|
if (!SvROK(scores_rv) || SvTYPE(SvRV(scores_rv)) != SVt_PVAV || |
|
315
|
|
|
|
|
|
|
!SvROK(labels_rv) || SvTYPE(SvRV(labels_rv)) != SVt_PVAV) { |
|
316
|
|
|
|
|
|
|
croak("score_predict_split_xs: scores/labels must be arrayrefs"); |
|
317
|
|
|
|
|
|
|
} |
|
318
|
|
|
|
|
|
|
sm = (const double*)SvPVbyte(sm_sv, tl); |
|
319
|
|
|
|
|
|
|
scores = (AV*)SvRV(scores_rv); |
|
320
|
|
|
|
|
|
|
labels = (AV*)SvRV(labels_rv); |
|
321
|
|
|
|
|
|
|
av_clear(scores); |
|
322
|
|
|
|
|
|
|
av_clear(labels); |
|
323
|
|
|
|
|
|
|
if (n_pts > 0) { |
|
324
|
|
|
|
|
|
|
av_extend(scores, n_pts - 1); |
|
325
|
|
|
|
|
|
|
av_extend(labels, n_pts - 1); |
|
326
|
|
|
|
|
|
|
} |
|
327
|
|
|
|
|
|
|
for (i = 0; i < n_pts; i++) { |
|
328
|
|
|
|
|
|
|
av_store(scores, i, newSVnv(exp(-sm[i] * inv))); |
|
329
|
|
|
|
|
|
|
av_store(labels, i, newSViv(sm[i] <= sum_threshold ? 1 : 0)); |
|
330
|
|
|
|
|
|
|
} |
|
331
|
|
|
|
|
|
|
} |
|
332
|
|
|
|
|
|
|
|
|
333
|
|
|
|
|
|
|
/* Walk one point through one tree; returns the path length (depth plus |
|
334
|
|
|
|
|
|
|
* the precomputed c(leaf size) adjustment from the leaf record). |
|
335
|
|
|
|
|
|
|
* |
|
336
|
|
|
|
|
|
|
* Invariant: every feature index stored in a tree node is in |
|
337
|
|
|
|
|
|
|
* [0, n_feats). fit() builds trees against n_features columns and |
|
338
|
|
|
|
|
|
|
* pack_input_xs writes exactly that many doubles per row, and |
|
339
|
|
|
|
|
|
|
* _resolve_input rejects PackedData with a mismatched feature count. |
|
340
|
|
|
|
|
|
|
* So the loop can omit per-iteration bounds checks on attr / fi -- |
|
341
|
|
|
|
|
|
|
* this is what lets the oblique dot product vectorize cleanly under |
|
342
|
|
|
|
|
|
|
* the omp-simd reductions below. */ |
|
343
|
|
|
|
|
|
|
#if defined(__GNUC__) || defined(__clang__) |
|
344
|
|
|
|
|
|
|
__attribute__((always_inline)) |
|
345
|
|
|
|
|
|
|
#endif |
|
346
|
|
|
|
|
|
|
static inline double if_walk_tree(const double *nd, const int *ico, |
|
347
|
|
|
|
|
|
|
const double *vco, const double *xi, |
|
348
|
|
|
|
|
|
|
int n_feats) { |
|
349
|
|
|
|
|
|
|
int ni = 0, depth = 0; |
|
350
|
|
|
|
|
|
|
for (;;) { |
|
351
|
|
|
|
|
|
|
const double *node = nd + (size_t)ni * IF_NZ; |
|
352
|
|
|
|
|
|
|
int type = (int)node[0]; |
|
353
|
|
|
|
|
|
|
if (type == 0) { |
|
354
|
|
|
|
|
|
|
/* node[2] is c(leaf size), precomputed by _pack_tree; a |
|
355
|
|
|
|
|
|
|
* log() here would otherwise run once per point per tree. */ |
|
356
|
|
|
|
|
|
|
return depth + node[2]; |
|
357
|
|
|
|
|
|
|
} |
|
358
|
|
|
|
|
|
|
if (type == 1) { |
|
359
|
|
|
|
|
|
|
double fv = xi[(int)node[1]]; |
|
360
|
|
|
|
|
|
|
ni = (fv < node[2]) ? (int)node[3] : (int)node[4]; |
|
361
|
|
|
|
|
|
|
} else { |
|
362
|
|
|
|
|
|
|
int coff = (int)node[1], nf = (int)node[2]; |
|
363
|
|
|
|
|
|
|
double b = node[5], dot = 0.0; |
|
364
|
|
|
|
|
|
|
const double *val_p = vco + (size_t)coff; |
|
365
|
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
/* Both children are known before the dot product resolves |
|
367
|
|
|
|
|
|
|
* which one gets taken, so start pulling their records in |
|
368
|
|
|
|
|
|
|
* now and let the FMA loop below hide the latency. One of |
|
369
|
|
|
|
|
|
|
* the two prefetches is always wasted -- affordable here |
|
370
|
|
|
|
|
|
|
* on the oblique path, where there is real work to hide it |
|
371
|
|
|
|
|
|
|
* under, but not on the axis path, whose single compare |
|
372
|
|
|
|
|
|
|
* resolves immediately. */ |
|
373
|
|
|
|
|
|
|
const int li = (int)node[3], ri = (int)node[4]; |
|
374
|
|
|
|
|
|
|
IF_PREFETCH(nd + (size_t)li * IF_NZ); |
|
375
|
|
|
|
|
|
|
IF_PREFETCH(nd + (size_t)ri * IF_NZ); |
|
376
|
|
|
|
|
|
|
if (nf == n_feats) { |
|
377
|
|
|
|
|
|
|
/* Dense oblique split: this node uses every feature, |
|
378
|
|
|
|
|
|
|
* so _pack_tree laid the coefficients out in feature |
|
379
|
|
|
|
|
|
|
* order. No gather -- the inner loop is a textbook |
|
380
|
|
|
|
|
|
|
* FMA-vectorizable dot product over two contiguous |
|
381
|
|
|
|
|
|
|
* double streams. Common case in extended mode at |
|
382
|
|
|
|
|
|
|
* the default extension_level (== n_feats-1). */ |
|
383
|
|
|
|
|
|
|
#ifdef _OPENMP |
|
384
|
|
|
|
|
|
|
#pragma omp simd reduction(+:dot) |
|
385
|
|
|
|
|
|
|
#endif |
|
386
|
|
|
|
|
|
|
for (int k = 0; k < n_feats; k++) { |
|
387
|
|
|
|
|
|
|
dot += val_p[k] * xi[k]; |
|
388
|
|
|
|
|
|
|
} |
|
389
|
|
|
|
|
|
|
} else { |
|
390
|
|
|
|
|
|
|
/* Sparse oblique split: only nf < n_feats features |
|
391
|
|
|
|
|
|
|
* participate, so we still need the gather on |
|
392
|
|
|
|
|
|
|
* xi[idx_p[k]]. Storing idx as contiguous int32 |
|
393
|
|
|
|
|
|
|
* (rather than interleaved doubles) keeps the gather |
|
394
|
|
|
|
|
|
|
* pattern clean and the val[] load contiguous. */ |
|
395
|
|
|
|
|
|
|
const int *idx_p = ico + (size_t)coff; |
|
396
|
|
|
|
|
|
|
#ifdef _OPENMP |
|
397
|
|
|
|
|
|
|
#pragma omp simd reduction(+:dot) |
|
398
|
|
|
|
|
|
|
#endif |
|
399
|
|
|
|
|
|
|
for (int k = 0; k < nf; k++) { |
|
400
|
|
|
|
|
|
|
dot += val_p[k] * xi[idx_p[k]]; |
|
401
|
|
|
|
|
|
|
} |
|
402
|
|
|
|
|
|
|
} |
|
403
|
|
|
|
|
|
|
ni = (dot <= b) ? li : ri; |
|
404
|
|
|
|
|
|
|
} |
|
405
|
|
|
|
|
|
|
depth++; |
|
406
|
|
|
|
|
|
|
} |
|
407
|
|
|
|
|
|
|
} |
|
408
|
|
|
|
|
|
|
|
|
409
|
|
|
|
|
|
|
/* score_all_xs(nodes_av, idx_av, val_av, x_sv, sm_sv, |
|
410
|
|
|
|
|
|
|
* n_pts, n_feats, n_trees, use_openmp) |
|
411
|
|
|
|
|
|
|
* |
|
412
|
|
|
|
|
|
|
* Scores all points across all trees in one C call. See header comment |
|
413
|
|
|
|
|
|
|
* above for the bigger picture. Writes sm[i] = sum_over_trees(path_len); |
|
414
|
|
|
|
|
|
|
* the caller need not zero-init sm. |
|
415
|
|
|
|
|
|
|
* |
|
416
|
|
|
|
|
|
|
* idx_av holds per-tree packed int32 buffers of feature indices and |
|
417
|
|
|
|
|
|
|
* val_av holds per-tree packed double buffers of coefficients (the SoA |
|
418
|
|
|
|
|
|
|
* counterpart of the old interleaved layout). See the file-top |
|
419
|
|
|
|
|
|
|
* comment for the rationale. |
|
420
|
|
|
|
|
|
|
* |
|
421
|
|
|
|
|
|
|
* Thread-safety: the parallel region only reads node/idx/val/x pointers |
|
422
|
|
|
|
|
|
|
* (extracted before the region) and writes sm[i] for a unique i per |
|
423
|
|
|
|
|
|
|
* iteration. No Perl API is called from inside the parallel region. */ |
|
424
|
|
|
|
|
|
|
void score_all_xs(SV* nodes_av_sv, SV* idx_av_sv, SV* val_av_sv, |
|
425
|
|
|
|
|
|
|
SV* x_sv, SV* sm_sv, |
|
426
|
|
|
|
|
|
|
int n_pts, int n_feats, int n_trees, |
|
427
|
|
|
|
|
|
|
int use_openmp){ |
|
428
|
|
|
|
|
|
|
STRLEN tl; |
|
429
|
|
|
|
|
|
|
AV *nodes_av, *idx_av, *val_av; |
|
430
|
|
|
|
|
|
|
const double *xd; |
|
431
|
|
|
|
|
|
|
double *sm; |
|
432
|
|
|
|
|
|
|
int ti; |
|
433
|
|
|
|
|
|
|
|
|
434
|
|
|
|
|
|
|
if (!SvROK(nodes_av_sv) || SvTYPE(SvRV(nodes_av_sv)) != SVt_PVAV || |
|
435
|
|
|
|
|
|
|
!SvROK(idx_av_sv) || SvTYPE(SvRV(idx_av_sv)) != SVt_PVAV || |
|
436
|
|
|
|
|
|
|
!SvROK(val_av_sv) || SvTYPE(SvRV(val_av_sv)) != SVt_PVAV) { |
|
437
|
|
|
|
|
|
|
croak("score_all_xs: nodes/idx/val must be arrayrefs"); |
|
438
|
|
|
|
|
|
|
} |
|
439
|
|
|
|
|
|
|
nodes_av = (AV*)SvRV(nodes_av_sv); |
|
440
|
|
|
|
|
|
|
idx_av = (AV*)SvRV(idx_av_sv); |
|
441
|
|
|
|
|
|
|
val_av = (AV*)SvRV(val_av_sv); |
|
442
|
|
|
|
|
|
|
|
|
443
|
|
|
|
|
|
|
/* C99 VLAs -- n_trees is small (typ. 100) and fits on the stack. */ |
|
444
|
|
|
|
|
|
|
const double *node_ptrs[n_trees]; |
|
445
|
|
|
|
|
|
|
const int *idx_ptrs[n_trees]; |
|
446
|
|
|
|
|
|
|
const double *val_ptrs[n_trees]; |
|
447
|
|
|
|
|
|
|
|
|
448
|
|
|
|
|
|
|
/* forest_bytes totals every buffer the tree walks touch; it decides |
|
449
|
|
|
|
|
|
|
* between the two loop shapes below. */ |
|
450
|
|
|
|
|
|
|
size_t forest_bytes = 0; |
|
451
|
|
|
|
|
|
|
for (ti = 0; ti < n_trees; ti++) { |
|
452
|
|
|
|
|
|
|
SV** np = av_fetch(nodes_av, ti, 0); |
|
453
|
|
|
|
|
|
|
SV** ip = av_fetch(idx_av, ti, 0); |
|
454
|
|
|
|
|
|
|
SV** vp = av_fetch(val_av, ti, 0); |
|
455
|
|
|
|
|
|
|
if (!np || !*np || !ip || !*ip || !vp || !*vp) { |
|
456
|
|
|
|
|
|
|
croak("score_all_xs: missing tree %d", ti); |
|
457
|
|
|
|
|
|
|
} |
|
458
|
|
|
|
|
|
|
node_ptrs[ti] = (const double*)SvPVbyte(*np, tl); forest_bytes += tl; |
|
459
|
|
|
|
|
|
|
idx_ptrs[ti] = (const int*) SvPVbyte(*ip, tl); forest_bytes += tl; |
|
460
|
|
|
|
|
|
|
val_ptrs[ti] = (const double*)SvPVbyte(*vp, tl); forest_bytes += tl; |
|
461
|
|
|
|
|
|
|
} |
|
462
|
|
|
|
|
|
|
|
|
463
|
|
|
|
|
|
|
xd = (const double*)SvPVbyte(x_sv, tl); |
|
464
|
|
|
|
|
|
|
sm = (double*)SvPVbyte_force(sm_sv, tl); |
|
465
|
|
|
|
|
|
|
|
|
466
|
|
|
|
|
|
|
/* Two loop shapes over the same per-point ascending-t additions -- |
|
467
|
|
|
|
|
|
|
* bit-identical results either way, so the size heuristic choosing |
|
468
|
|
|
|
|
|
|
* between them can never change scores. |
|
469
|
|
|
|
|
|
|
* |
|
470
|
|
|
|
|
|
|
* Point-major (small forests): each point walks all trees with its |
|
471
|
|
|
|
|
|
|
* path-length sum held in a register. Cheapest per walk, and the |
|
472
|
|
|
|
|
|
|
* whole forest stays cache-resident across points anyway. |
|
473
|
|
|
|
|
|
|
* |
|
474
|
|
|
|
|
|
|
* Tree-blocked (large forests): once the forest outgrows L3, the |
|
475
|
|
|
|
|
|
|
* point-major loop re-streams every tree's nodes and coefficients |
|
476
|
|
|
|
|
|
|
* from memory for every point -- an extended-mode tree is ~56 KB |
|
477
|
|
|
|
|
|
|
* at 16 features (24 KB nodes + 32 KB dense coefficients), and its |
|
478
|
|
|
|
|
|
|
* per-tree scoring cost measured 2.2x worse at 400 trees than at |
|
479
|
|
|
|
|
|
|
* 100. Walking a block of points through ONE tree at a time keeps |
|
480
|
|
|
|
|
|
|
* that tree hot in L1/L2 while the block's rows stream through it |
|
481
|
|
|
|
|
|
|
* (measured 3.1x faster at 400 extended trees, 20k points). The |
|
482
|
|
|
|
|
|
|
* blocked shape pays an sm[i] load+store per walk instead of a |
|
483
|
|
|
|
|
|
|
* register add, which measurably hurts cheap axis walks while the |
|
484
|
|
|
|
|
|
|
* forest still fits in cache -- hence the byte threshold rather |
|
485
|
|
|
|
|
|
|
* than always tiling. */ |
|
486
|
|
|
|
|
|
|
if (forest_bytes <= (size_t)4 * 1024 * 1024) { |
|
487
|
|
|
|
|
|
|
#ifdef _OPENMP |
|
488
|
|
|
|
|
|
|
#pragma omp parallel for schedule(static) if(use_openmp) |
|
489
|
|
|
|
|
|
|
#endif |
|
490
|
|
|
|
|
|
|
for (int i = 0; i < n_pts; i++) { |
|
491
|
|
|
|
|
|
|
const double *xi = xd + (size_t)i * (size_t)n_feats; |
|
492
|
|
|
|
|
|
|
double sum = 0.0; |
|
493
|
|
|
|
|
|
|
for (int t = 0; t < n_trees; t++) { |
|
494
|
|
|
|
|
|
|
sum += if_walk_tree(node_ptrs[t], idx_ptrs[t], |
|
495
|
|
|
|
|
|
|
val_ptrs[t], xi, n_feats); |
|
496
|
|
|
|
|
|
|
} |
|
497
|
|
|
|
|
|
|
sm[i] = sum; |
|
498
|
|
|
|
|
|
|
} |
|
499
|
|
|
|
|
|
|
} |
|
500
|
|
|
|
|
|
|
else { |
|
501
|
|
|
|
|
|
|
/* 256 rows x 16 features x 8 bytes = 32 KB of input per block |
|
502
|
|
|
|
|
|
|
* -- comfortable in L2 next to one tree. Each OpenMP thread |
|
503
|
|
|
|
|
|
|
* owns whole blocks and therefore a unique slice of sm[], so |
|
504
|
|
|
|
|
|
|
* there is still no synchronisation. For small batches the |
|
505
|
|
|
|
|
|
|
* tile shrinks to keep ~4 blocks per thread available; losing |
|
506
|
|
|
|
|
|
|
* per-block tree reuse there is fine, since a small batch |
|
507
|
|
|
|
|
|
|
* never re-streams much anyway. */ |
|
508
|
|
|
|
|
|
|
int tile = 256; |
|
509
|
|
|
|
|
|
|
#ifdef _OPENMP |
|
510
|
|
|
|
|
|
|
if (use_openmp) { |
|
511
|
|
|
|
|
|
|
int min_blocks = omp_get_max_threads() * 4; |
|
512
|
|
|
|
|
|
|
if (min_blocks > 0 && (n_pts + tile - 1) / tile < min_blocks) { |
|
513
|
|
|
|
|
|
|
tile = (n_pts + min_blocks - 1) / min_blocks; |
|
514
|
|
|
|
|
|
|
if (tile < 1) tile = 1; |
|
515
|
|
|
|
|
|
|
} |
|
516
|
|
|
|
|
|
|
} |
|
517
|
|
|
|
|
|
|
#endif |
|
518
|
|
|
|
|
|
|
int n_blocks = (n_pts + tile - 1) / tile; |
|
519
|
|
|
|
|
|
|
|
|
520
|
|
|
|
|
|
|
#ifdef _OPENMP |
|
521
|
|
|
|
|
|
|
#pragma omp parallel for schedule(static) if(use_openmp) |
|
522
|
|
|
|
|
|
|
#endif |
|
523
|
|
|
|
|
|
|
for (int blk = 0; blk < n_blocks; blk++) { |
|
524
|
|
|
|
|
|
|
const int i0 = blk * tile; |
|
525
|
|
|
|
|
|
|
const int i1 = (i0 + tile < n_pts) ? i0 + tile : n_pts; |
|
526
|
|
|
|
|
|
|
for (int i = i0; i < i1; i++) sm[i] = 0.0; |
|
527
|
|
|
|
|
|
|
for (int t = 0; t < n_trees; t++) { |
|
528
|
|
|
|
|
|
|
const double *nd = node_ptrs[t]; |
|
529
|
|
|
|
|
|
|
const int *ico = idx_ptrs[t]; |
|
530
|
|
|
|
|
|
|
const double *vco = val_ptrs[t]; |
|
531
|
|
|
|
|
|
|
for (int i = i0; i < i1; i++) { |
|
532
|
|
|
|
|
|
|
sm[i] += if_walk_tree(nd, ico, vco, |
|
533
|
|
|
|
|
|
|
xd + (size_t)i * (size_t)n_feats, |
|
534
|
|
|
|
|
|
|
n_feats); |
|
535
|
|
|
|
|
|
|
} |
|
536
|
|
|
|
|
|
|
} |
|
537
|
|
|
|
|
|
|
} |
|
538
|
|
|
|
|
|
|
} |
|
539
|
|
|
|
|
|
|
} |
|
540
|
|
|
|
|
|
|
|
|
541
|
|
|
|
|
|
|
/* --------------------------------------------------------------------- |
|
542
|
|
|
|
|
|
|
* build_forest_xs -- C-accelerated fit() tree builder. |
|
543
|
|
|
|
|
|
|
* |
|
544
|
|
|
|
|
|
|
* Replaces the pure-Perl _subsample + _build_tree + _axis_split / |
|
545
|
|
|
|
|
|
|
* _oblique_split recursion with an equivalent C implementation that |
|
546
|
|
|
|
|
|
|
* partitions plain `int` row-index arrays instead of copying arrayrefs |
|
547
|
|
|
|
|
|
|
* of Perl SVs at every split. Random draws go through Drand01() -- |
|
548
|
|
|
|
|
|
|
* the exact generator Perl's own rand()/srand() use internally -- in |
|
549
|
|
|
|
|
|
|
* the same call order the Perl code used, so a fit() with a given |
|
550
|
|
|
|
|
|
|
* seed produces BIT-IDENTICAL trees whether use_c is on or off. This |
|
551
|
|
|
|
|
|
|
* is what lets fit() reuse the existing `use_c` knob instead of a new |
|
552
|
|
|
|
|
|
|
* one: switching backends never changes the model, only how fast it's |
|
553
|
|
|
|
|
|
|
* built. (Verified by t/02-accel-selection.t's "identical seed => |
|
554
|
|
|
|
|
|
|
* identical trees" subtest, which exercises both backends.) |
|
555
|
|
|
|
|
|
|
* |
|
556
|
|
|
|
|
|
|
* Output trees are plain Perl arrayrefs in the same node shape |
|
557
|
|
|
|
|
|
|
* _build_tree produces (leaf/axis/oblique -- see the file-top |
|
558
|
|
|
|
|
|
|
* comment), so every downstream consumer (_pack_tree, to_json, |
|
559
|
|
|
|
|
|
|
* from_json, the pure-Perl scorer) is unchanged. |
|
560
|
|
|
|
|
|
|
* |
|
561
|
|
|
|
|
|
|
* x_sv: packed row-major double buffer, n_pts rows of n_feats each |
|
562
|
|
|
|
|
|
|
* (from pack_input_xs -- NaN marks a missing cell under the |
|
563
|
|
|
|
|
|
|
* 'nan' missing-strategy). |
|
564
|
|
|
|
|
|
|
* mode_flag: 0 => axis-parallel splits, 1 => oblique (extended). |
|
565
|
|
|
|
|
|
|
* ext_level: extension_level_used (ignored when mode_flag == 0). |
|
566
|
|
|
|
|
|
|
* out_rv: pre-existing arrayref; filled with n_trees tree roots. |
|
567
|
|
|
|
|
|
|
* ------------------------------------------------------------------ */ |
|
568
|
|
|
|
|
|
|
|
|
569
|
|
|
|
|
|
|
/* Box-Muller normal draw, in the same rand() call order as _randn(). */ |
|
570
|
|
|
|
|
|
|
static double _c_randn(pTHX) { |
|
571
|
|
|
|
|
|
|
double u1 = Drand01(); |
|
572
|
|
|
|
|
|
|
double u2; |
|
573
|
|
|
|
|
|
|
if (u1 == 0.0) u1 = 1e-12; |
|
574
|
|
|
|
|
|
|
u2 = Drand01(); |
|
575
|
|
|
|
|
|
|
return sqrt(-2.0 * log(u1)) * cos(6.283185307179586 * u2); |
|
576
|
|
|
|
|
|
|
} |
|
577
|
|
|
|
|
|
|
|
|
578
|
|
|
|
|
|
|
static SV* _mk_leaf(pTHX_ int size) { |
|
579
|
|
|
|
|
|
|
AV* av = newAV(); |
|
580
|
|
|
|
|
|
|
av_extend(av, 1); |
|
581
|
|
|
|
|
|
|
AvARRAY(av)[0] = newSVnv(0.0); |
|
582
|
|
|
|
|
|
|
AvARRAY(av)[1] = newSViv(size); |
|
583
|
|
|
|
|
|
|
AvFILLp(av) = 1; |
|
584
|
|
|
|
|
|
|
return newRV_noinc((SV*)av); |
|
585
|
|
|
|
|
|
|
} |
|
586
|
|
|
|
|
|
|
|
|
587
|
|
|
|
|
|
|
static SV* _mk_axis(pTHX_ int attr, double split, SV* left, SV* right) { |
|
588
|
|
|
|
|
|
|
AV* av = newAV(); |
|
589
|
|
|
|
|
|
|
av_extend(av, 4); |
|
590
|
|
|
|
|
|
|
AvARRAY(av)[0] = newSVnv(1.0); |
|
591
|
|
|
|
|
|
|
AvARRAY(av)[1] = newSViv(attr); |
|
592
|
|
|
|
|
|
|
AvARRAY(av)[2] = newSVnv(split); |
|
593
|
|
|
|
|
|
|
AvARRAY(av)[3] = left; |
|
594
|
|
|
|
|
|
|
AvARRAY(av)[4] = right; |
|
595
|
|
|
|
|
|
|
AvFILLp(av) = 4; |
|
596
|
|
|
|
|
|
|
return newRV_noinc((SV*)av); |
|
597
|
|
|
|
|
|
|
} |
|
598
|
|
|
|
|
|
|
|
|
599
|
|
|
|
|
|
|
static SV* _mk_oblique(pTHX_ const int* idx, const double* coef, int n, |
|
600
|
|
|
|
|
|
|
double b, SV* left, SV* right) { |
|
601
|
|
|
|
|
|
|
AV *iav, *cav, *av; |
|
602
|
|
|
|
|
|
|
int k; |
|
603
|
|
|
|
|
|
|
iav = newAV(); |
|
604
|
|
|
|
|
|
|
cav = newAV(); |
|
605
|
|
|
|
|
|
|
if (n > 0) { |
|
606
|
|
|
|
|
|
|
av_extend(iav, n - 1); |
|
607
|
|
|
|
|
|
|
av_extend(cav, n - 1); |
|
608
|
|
|
|
|
|
|
} |
|
609
|
|
|
|
|
|
|
for (k = 0; k < n; k++) { |
|
610
|
|
|
|
|
|
|
AvARRAY(iav)[k] = newSViv(idx[k]); |
|
611
|
|
|
|
|
|
|
AvARRAY(cav)[k] = newSVnv(coef[k]); |
|
612
|
|
|
|
|
|
|
} |
|
613
|
|
|
|
|
|
|
AvFILLp(iav) = n - 1; |
|
614
|
|
|
|
|
|
|
AvFILLp(cav) = n - 1; |
|
615
|
|
|
|
|
|
|
|
|
616
|
|
|
|
|
|
|
av = newAV(); |
|
617
|
|
|
|
|
|
|
av_extend(av, 5); |
|
618
|
|
|
|
|
|
|
AvARRAY(av)[0] = newSVnv(2.0); |
|
619
|
|
|
|
|
|
|
AvARRAY(av)[1] = newRV_noinc((SV*)iav); |
|
620
|
|
|
|
|
|
|
AvARRAY(av)[2] = newRV_noinc((SV*)cav); |
|
621
|
|
|
|
|
|
|
AvARRAY(av)[3] = newSVnv(b); |
|
622
|
|
|
|
|
|
|
AvARRAY(av)[4] = left; |
|
623
|
|
|
|
|
|
|
AvARRAY(av)[5] = right; |
|
624
|
|
|
|
|
|
|
AvFILLp(av) = 5; |
|
625
|
|
|
|
|
|
|
return newRV_noinc((SV*)av); |
|
626
|
|
|
|
|
|
|
} |
|
627
|
|
|
|
|
|
|
|
|
628
|
|
|
|
|
|
|
/* Builds one node from the point set `idxs` (row indices into `x`, |
|
629
|
|
|
|
|
|
|
* length `size`); recurses left-then-right, matching _build_tree's |
|
630
|
|
|
|
|
|
|
* traversal order so nested splits draw random numbers in the same |
|
631
|
|
|
|
|
|
|
* sequence the pure-Perl path would. Takes ownership of `idxs` -- |
|
632
|
|
|
|
|
|
|
* frees it before returning. */ |
|
633
|
|
|
|
|
|
|
static SV* _build_node_c(pTHX_ const double* x, int nf, int* idxs, int size, |
|
634
|
|
|
|
|
|
|
int depth, int limit, int mode_flag, |
|
635
|
|
|
|
|
|
|
int ext_active) { |
|
636
|
|
|
|
|
|
|
double *lo, *hi; |
|
637
|
|
|
|
|
|
|
int *varying, nv, f; |
|
638
|
|
|
|
|
|
|
SV *result; |
|
639
|
|
|
|
|
|
|
|
|
640
|
|
|
|
|
|
|
if (depth >= limit || size <= 1) { |
|
641
|
|
|
|
|
|
|
SV* leaf = _mk_leaf(aTHX_ size); |
|
642
|
|
|
|
|
|
|
free(idxs); |
|
643
|
|
|
|
|
|
|
return leaf; |
|
644
|
|
|
|
|
|
|
} |
|
645
|
|
|
|
|
|
|
|
|
646
|
|
|
|
|
|
|
lo = (double*)malloc(nf * sizeof(double)); |
|
647
|
|
|
|
|
|
|
hi = (double*)malloc(nf * sizeof(double)); |
|
648
|
|
|
|
|
|
|
for (f = 0; f < nf; f++) { |
|
649
|
|
|
|
|
|
|
lo[f] = HUGE_VAL; |
|
650
|
|
|
|
|
|
|
hi[f] = -HUGE_VAL; |
|
651
|
|
|
|
|
|
|
} |
|
652
|
|
|
|
|
|
|
for (int i = 0; i < size; i++) { |
|
653
|
|
|
|
|
|
|
const double* row = x + (size_t)idxs[i] * (size_t)nf; |
|
654
|
|
|
|
|
|
|
/* No isnan() guard needed: NaN < x and NaN > x are always false |
|
655
|
|
|
|
|
|
|
* under IEEE 754, so a NaN cell (the 'nan' missing strategy) |
|
656
|
|
|
|
|
|
|
* already leaves lo/hi untouched without an explicit check -- |
|
657
|
|
|
|
|
|
|
* one less branch, and it's what lets this loop vectorize |
|
658
|
|
|
|
|
|
|
* cleanly as a plain elementwise min/max scan. */ |
|
659
|
|
|
|
|
|
|
#ifdef _OPENMP |
|
660
|
|
|
|
|
|
|
#pragma omp simd |
|
661
|
|
|
|
|
|
|
#endif |
|
662
|
|
|
|
|
|
|
for (int f2 = 0; f2 < nf; f2++) { |
|
663
|
|
|
|
|
|
|
double v = row[f2]; |
|
664
|
|
|
|
|
|
|
if (v < lo[f2]) lo[f2] = v; |
|
665
|
|
|
|
|
|
|
if (v > hi[f2]) hi[f2] = v; |
|
666
|
|
|
|
|
|
|
} |
|
667
|
|
|
|
|
|
|
} |
|
668
|
|
|
|
|
|
|
|
|
669
|
|
|
|
|
|
|
varying = (int*)malloc(nf * sizeof(int)); |
|
670
|
|
|
|
|
|
|
nv = 0; |
|
671
|
|
|
|
|
|
|
for (f = 0; f < nf; f++) { |
|
672
|
|
|
|
|
|
|
if (lo[f] < hi[f]) varying[nv++] = f; |
|
673
|
|
|
|
|
|
|
} |
|
674
|
|
|
|
|
|
|
|
|
675
|
|
|
|
|
|
|
if (nv == 0) { |
|
676
|
|
|
|
|
|
|
free(lo); free(hi); free(varying); |
|
677
|
|
|
|
|
|
|
SV* leaf = _mk_leaf(aTHX_ size); |
|
678
|
|
|
|
|
|
|
free(idxs); |
|
679
|
|
|
|
|
|
|
return leaf; |
|
680
|
|
|
|
|
|
|
} |
|
681
|
|
|
|
|
|
|
|
|
682
|
|
|
|
|
|
|
if (mode_flag == 0) { |
|
683
|
|
|
|
|
|
|
/* Axis-parallel split: one varying feature, one threshold. */ |
|
684
|
|
|
|
|
|
|
int attr = varying[(int)(Drand01() * nv)]; |
|
685
|
|
|
|
|
|
|
double split = lo[attr] + Drand01() * (hi[attr] - lo[attr]); |
|
686
|
|
|
|
|
|
|
int *lidx = (int*)malloc(size * sizeof(int)); |
|
687
|
|
|
|
|
|
|
int *ridx = (int*)malloc(size * sizeof(int)); |
|
688
|
|
|
|
|
|
|
int ln = 0, rn = 0, i; |
|
689
|
|
|
|
|
|
|
SV *left, *right; |
|
690
|
|
|
|
|
|
|
|
|
691
|
|
|
|
|
|
|
for (i = 0; i < size; i++) { |
|
692
|
|
|
|
|
|
|
int row = idxs[i]; |
|
693
|
|
|
|
|
|
|
double v = x[(size_t)row * (size_t)nf + attr]; |
|
694
|
|
|
|
|
|
|
if (v < split) lidx[ln++] = row; else ridx[rn++] = row; |
|
695
|
|
|
|
|
|
|
} |
|
696
|
|
|
|
|
|
|
free(idxs); free(lo); free(hi); free(varying); |
|
697
|
|
|
|
|
|
|
|
|
698
|
|
|
|
|
|
|
left = _build_node_c(aTHX_ x, nf, lidx, ln, depth + 1, limit, |
|
699
|
|
|
|
|
|
|
mode_flag, ext_active); |
|
700
|
|
|
|
|
|
|
right = _build_node_c(aTHX_ x, nf, ridx, rn, depth + 1, limit, |
|
701
|
|
|
|
|
|
|
mode_flag, ext_active); |
|
702
|
|
|
|
|
|
|
result = _mk_axis(aTHX_ attr, split, left, right); |
|
703
|
|
|
|
|
|
|
} else { |
|
704
|
|
|
|
|
|
|
/* Oblique split: a random hyperplane over `active` features. */ |
|
705
|
|
|
|
|
|
|
int active = ext_active + 1; |
|
706
|
|
|
|
|
|
|
int *pool, *lidx, *ridx; |
|
707
|
|
|
|
|
|
|
double *coef; |
|
708
|
|
|
|
|
|
|
double b = 0.0; |
|
709
|
|
|
|
|
|
|
int ln = 0, rn = 0, i, k; |
|
710
|
|
|
|
|
|
|
SV *left, *right; |
|
711
|
|
|
|
|
|
|
|
|
712
|
|
|
|
|
|
|
if (active > nv) active = nv; |
|
713
|
|
|
|
|
|
|
pool = (int*)malloc(nv * sizeof(int)); |
|
714
|
|
|
|
|
|
|
memcpy(pool, varying, nv * sizeof(int)); |
|
715
|
|
|
|
|
|
|
for (i = 0; i < active; i++) { |
|
716
|
|
|
|
|
|
|
int j = i + (int)(Drand01() * (nv - i)); |
|
717
|
|
|
|
|
|
|
int tmp = pool[i]; pool[i] = pool[j]; pool[j] = tmp; |
|
718
|
|
|
|
|
|
|
} |
|
719
|
|
|
|
|
|
|
|
|
720
|
|
|
|
|
|
|
coef = (double*)malloc(active * sizeof(double)); |
|
721
|
|
|
|
|
|
|
for (k = 0; k < active; k++) { |
|
722
|
|
|
|
|
|
|
int ff = pool[k]; |
|
723
|
|
|
|
|
|
|
double c = _c_randn(aTHX); |
|
724
|
|
|
|
|
|
|
double p = lo[ff] + Drand01() * (hi[ff] - lo[ff]); |
|
725
|
|
|
|
|
|
|
coef[k] = c; |
|
726
|
|
|
|
|
|
|
b += c * p; |
|
727
|
|
|
|
|
|
|
} |
|
728
|
|
|
|
|
|
|
|
|
729
|
|
|
|
|
|
|
lidx = (int*)malloc(size * sizeof(int)); |
|
730
|
|
|
|
|
|
|
ridx = (int*)malloc(size * sizeof(int)); |
|
731
|
|
|
|
|
|
|
for (i = 0; i < size; i++) { |
|
732
|
|
|
|
|
|
|
int row = idxs[i]; |
|
733
|
|
|
|
|
|
|
double dot = 0.0; |
|
734
|
|
|
|
|
|
|
for (k = 0; k < active; k++) { |
|
735
|
|
|
|
|
|
|
dot += coef[k] * x[(size_t)row * (size_t)nf + pool[k]]; |
|
736
|
|
|
|
|
|
|
} |
|
737
|
|
|
|
|
|
|
if (dot <= b) lidx[ln++] = row; else ridx[rn++] = row; |
|
738
|
|
|
|
|
|
|
} |
|
739
|
|
|
|
|
|
|
free(idxs); free(lo); free(hi); free(varying); |
|
740
|
|
|
|
|
|
|
|
|
741
|
|
|
|
|
|
|
left = _build_node_c(aTHX_ x, nf, lidx, ln, depth + 1, limit, |
|
742
|
|
|
|
|
|
|
mode_flag, ext_active); |
|
743
|
|
|
|
|
|
|
right = _build_node_c(aTHX_ x, nf, ridx, rn, depth + 1, limit, |
|
744
|
|
|
|
|
|
|
mode_flag, ext_active); |
|
745
|
|
|
|
|
|
|
result = _mk_oblique(aTHX_ pool, coef, active, b, left, right); |
|
746
|
|
|
|
|
|
|
free(pool); free(coef); |
|
747
|
|
|
|
|
|
|
} |
|
748
|
|
|
|
|
|
|
return result; |
|
749
|
|
|
|
|
|
|
} |
|
750
|
|
|
|
|
|
|
|
|
751
|
|
|
|
|
|
|
void build_forest_xs(SV* x_sv, int n_pts, int n_feats, int n_trees, |
|
752
|
|
|
|
|
|
|
int psi, int limit, int mode_flag, int ext_level, |
|
753
|
|
|
|
|
|
|
SV* out_rv) { |
|
754
|
|
|
|
|
|
|
dTHX; |
|
755
|
|
|
|
|
|
|
STRLEN tl; |
|
756
|
|
|
|
|
|
|
const double* x; |
|
757
|
|
|
|
|
|
|
AV* out; |
|
758
|
|
|
|
|
|
|
int* all; |
|
759
|
|
|
|
|
|
|
int t, i; |
|
760
|
|
|
|
|
|
|
|
|
761
|
|
|
|
|
|
|
if (!SvROK(out_rv) || SvTYPE(SvRV(out_rv)) != SVt_PVAV) { |
|
762
|
|
|
|
|
|
|
croak("build_forest_xs: out must be an arrayref"); |
|
763
|
|
|
|
|
|
|
} |
|
764
|
|
|
|
|
|
|
x = (const double*)SvPVbyte(x_sv, tl); |
|
765
|
|
|
|
|
|
|
out = (AV*)SvRV(out_rv); |
|
766
|
|
|
|
|
|
|
av_clear(out); |
|
767
|
|
|
|
|
|
|
if (n_trees > 0) av_extend(out, n_trees - 1); |
|
768
|
|
|
|
|
|
|
|
|
769
|
|
|
|
|
|
|
all = (int*)malloc(n_pts * sizeof(int)); |
|
770
|
|
|
|
|
|
|
for (t = 0; t < n_trees; t++) { |
|
771
|
|
|
|
|
|
|
int* sample; |
|
772
|
|
|
|
|
|
|
|
|
773
|
|
|
|
|
|
|
for (i = 0; i < n_pts; i++) all[i] = i; |
|
774
|
|
|
|
|
|
|
for (i = 0; i < psi; i++) { |
|
775
|
|
|
|
|
|
|
int j = i + (int)(Drand01() * (n_pts - i)); |
|
776
|
|
|
|
|
|
|
int tmp = all[i]; all[i] = all[j]; all[j] = tmp; |
|
777
|
|
|
|
|
|
|
} |
|
778
|
|
|
|
|
|
|
sample = (int*)malloc(psi * sizeof(int)); |
|
779
|
|
|
|
|
|
|
memcpy(sample, all, psi * sizeof(int)); |
|
780
|
|
|
|
|
|
|
|
|
781
|
|
|
|
|
|
|
av_store(out, t, |
|
782
|
|
|
|
|
|
|
_build_node_c(aTHX_ x, n_feats, sample, psi, 0, limit, |
|
783
|
|
|
|
|
|
|
mode_flag, ext_level)); |
|
784
|
|
|
|
|
|
|
} |
|
785
|
|
|
|
|
|
|
free(all); |
|
786
|
|
|
|
|
|
|
} |
|
787
|
|
|
|
|
|
|
|
|
788
|
|
|
|
|
|
|
/* --------------------------------------------------------------------- |
|
789
|
|
|
|
|
|
|
* build_forest_openmp_xs -- OpenMP-parallel fit() tree builder. |
|
790
|
|
|
|
|
|
|
* |
|
791
|
|
|
|
|
|
|
* build_forest_xs (above) is bit-identical to the pure-Perl path |
|
792
|
|
|
|
|
|
|
* because every random draw goes through Drand01(), the same |
|
793
|
|
|
|
|
|
|
* generator Perl's rand()/srand() use -- but that generator is a |
|
794
|
|
|
|
|
|
|
* single mutable struct shared by the whole interpreter, so calling |
|
795
|
|
|
|
|
|
|
* it concurrently from multiple OpenMP threads would be a data race. |
|
796
|
|
|
|
|
|
|
* The same is true of any Perl API call (newAV, newSViv, ...): Perl's |
|
797
|
|
|
|
|
|
|
* SV allocator isn't safe to call from multiple OS threads sharing one |
|
798
|
|
|
|
|
|
|
* interpreter without a lock that would just serialise everything |
|
799
|
|
|
|
|
|
|
* anyway. |
|
800
|
|
|
|
|
|
|
* |
|
801
|
|
|
|
|
|
|
* So this builder trades the bit-identical guarantee for real thread |
|
802
|
|
|
|
|
|
|
* parallelism: each tree gets its own splitmix64 PRNG stream, seeded |
|
803
|
|
|
|
|
|
|
* from a tree index (not thread id or scheduling order), so results |
|
804
|
|
|
|
|
|
|
* are still reproducible for a fixed seed and n_trees regardless of |
|
805
|
|
|
|
|
|
|
* OMP_NUM_THREADS -- just different from what build_forest_xs or the |
|
806
|
|
|
|
|
|
|
* pure-Perl path would produce for the same seed. The one Drand01() |
|
807
|
|
|
|
|
|
|
* call in this function happens before the parallel region starts |
|
808
|
|
|
|
|
|
|
* (single-threaded), so the result still varies with the model's |
|
809
|
|
|
|
|
|
|
* `seed` the way every other code path does; it isn't used inside the |
|
810
|
|
|
|
|
|
|
* parallel loop. |
|
811
|
|
|
|
|
|
|
* |
|
812
|
|
|
|
|
|
|
* Each tree is built entirely with plain C data (row-index int arrays, |
|
813
|
|
|
|
|
|
|
* a growable TreeBuf of packed doubles/ints) -- no Perl API call |
|
814
|
|
|
|
|
|
|
* happens anywhere inside the parallel region. Each node record in |
|
815
|
|
|
|
|
|
|
* TreeBuf uses _pack_tree's 6-double SoA layout (see the file-top |
|
816
|
|
|
|
|
|
|
* comment), but the node ORDER differs: records are appended |
|
817
|
|
|
|
|
|
|
* post-order (a node is pushed after both its children, since child |
|
818
|
|
|
|
|
|
|
* indices must be known first), so the root is the last record -- |
|
819
|
|
|
|
|
|
|
* _pack_tree's pre-order puts it at 0. _unpack_forest accounts for |
|
820
|
|
|
|
|
|
|
* this. Oblique coefficients are also always stored sparse (in the |
|
821
|
|
|
|
|
|
|
* random pool's order) -- the dense-pack fast path is skipped because |
|
822
|
|
|
|
|
|
|
* its only purpose is speeding up score_all_xs, and _rebuild_c_trees |
|
823
|
|
|
|
|
|
|
* reapplies it anyway once the caller unpacks these buffers back into |
|
824
|
|
|
|
|
|
|
* the standard Perl tree shape and re-derives the scoring buffers. |
|
825
|
|
|
|
|
|
|
* |
|
826
|
|
|
|
|
|
|
* After the parallel region, each tree's TreeBuf is copied into a Perl |
|
827
|
|
|
|
|
|
|
* string SV (one memcpy each, serially) and stored into nodes_rv / |
|
828
|
|
|
|
|
|
|
* idx_rv / val_rv -- the caller unpacks these into ordinary nested |
|
829
|
|
|
|
|
|
|
* Perl trees for $self->{trees} (so to_json/persistence/_rebuild_c_trees |
|
830
|
|
|
|
|
|
|
* are unaffected). ------------------------------------------------ */ |
|
831
|
|
|
|
|
|
|
|
|
832
|
|
|
|
|
|
|
typedef struct { |
|
833
|
|
|
|
|
|
|
double *nodes; size_t n_nodes, cap_nodes; |
|
834
|
|
|
|
|
|
|
int *idx; size_t n_idx, cap_idx; |
|
835
|
|
|
|
|
|
|
double *val; size_t n_val, cap_val; |
|
836
|
|
|
|
|
|
|
} TreeBuf; |
|
837
|
|
|
|
|
|
|
|
|
838
|
|
|
|
|
|
|
static void tb_init(TreeBuf *b) { |
|
839
|
|
|
|
|
|
|
b->nodes = NULL; b->n_nodes = 0; b->cap_nodes = 0; |
|
840
|
|
|
|
|
|
|
b->idx = NULL; b->n_idx = 0; b->cap_idx = 0; |
|
841
|
|
|
|
|
|
|
b->val = NULL; b->n_val = 0; b->cap_val = 0; |
|
842
|
|
|
|
|
|
|
} |
|
843
|
|
|
|
|
|
|
|
|
844
|
|
|
|
|
|
|
static void tb_free(TreeBuf *b) { |
|
845
|
|
|
|
|
|
|
free(b->nodes); free(b->idx); free(b->val); |
|
846
|
|
|
|
|
|
|
} |
|
847
|
|
|
|
|
|
|
|
|
848
|
|
|
|
|
|
|
static int tb_push_node(TreeBuf *b, double f0, double f1, double f2, |
|
849
|
|
|
|
|
|
|
double f3, double f4, double f5) { |
|
850
|
|
|
|
|
|
|
double *slot; |
|
851
|
|
|
|
|
|
|
if (b->n_nodes == b->cap_nodes) { |
|
852
|
|
|
|
|
|
|
size_t newcap = b->cap_nodes ? b->cap_nodes * 2 : 64; |
|
853
|
|
|
|
|
|
|
b->nodes = (double*)realloc(b->nodes, newcap * 6 * sizeof(double)); |
|
854
|
|
|
|
|
|
|
b->cap_nodes = newcap; |
|
855
|
|
|
|
|
|
|
} |
|
856
|
|
|
|
|
|
|
slot = b->nodes + b->n_nodes * 6; |
|
857
|
|
|
|
|
|
|
slot[0] = f0; slot[1] = f1; slot[2] = f2; |
|
858
|
|
|
|
|
|
|
slot[3] = f3; slot[4] = f4; slot[5] = f5; |
|
859
|
|
|
|
|
|
|
return (int)(b->n_nodes++); |
|
860
|
|
|
|
|
|
|
} |
|
861
|
|
|
|
|
|
|
|
|
862
|
|
|
|
|
|
|
/* Appends n (idx[k], val[k]) pairs and returns the offset they start |
|
863
|
|
|
|
|
|
|
* at -- the `coff` an oblique node record stores. */ |
|
864
|
|
|
|
|
|
|
static int tb_push_coef(TreeBuf *b, const int *idx, const double *val, |
|
865
|
|
|
|
|
|
|
int n) { |
|
866
|
|
|
|
|
|
|
int off = (int)b->n_idx; |
|
867
|
|
|
|
|
|
|
if (b->n_idx + (size_t)n > b->cap_idx) { |
|
868
|
|
|
|
|
|
|
size_t newcap = b->cap_idx ? b->cap_idx * 2 : 64; |
|
869
|
|
|
|
|
|
|
if (newcap < b->n_idx + (size_t)n) newcap = b->n_idx + (size_t)n; |
|
870
|
|
|
|
|
|
|
b->idx = (int*)realloc(b->idx, newcap * sizeof(int)); |
|
871
|
|
|
|
|
|
|
b->cap_idx = newcap; |
|
872
|
|
|
|
|
|
|
} |
|
873
|
|
|
|
|
|
|
if (b->n_val + (size_t)n > b->cap_val) { |
|
874
|
|
|
|
|
|
|
size_t newcap = b->cap_val ? b->cap_val * 2 : 64; |
|
875
|
|
|
|
|
|
|
if (newcap < b->n_val + (size_t)n) newcap = b->n_val + (size_t)n; |
|
876
|
|
|
|
|
|
|
b->val = (double*)realloc(b->val, newcap * sizeof(double)); |
|
877
|
|
|
|
|
|
|
b->cap_val = newcap; |
|
878
|
|
|
|
|
|
|
} |
|
879
|
|
|
|
|
|
|
memcpy(b->idx + b->n_idx, idx, (size_t)n * sizeof(int)); |
|
880
|
|
|
|
|
|
|
memcpy(b->val + b->n_val, val, (size_t)n * sizeof(double)); |
|
881
|
|
|
|
|
|
|
b->n_idx += n; |
|
882
|
|
|
|
|
|
|
b->n_val += n; |
|
883
|
|
|
|
|
|
|
return off; |
|
884
|
|
|
|
|
|
|
} |
|
885
|
|
|
|
|
|
|
|
|
886
|
|
|
|
|
|
|
/* splitmix64 -- fast, well-mixed, and per-stream state fits in one |
|
887
|
|
|
|
|
|
|
* uint64_t, which is all a thread-private PRNG needs here. Not |
|
888
|
|
|
|
|
|
|
* cryptographic; doesn't need to be. */ |
|
889
|
|
|
|
|
|
|
static uint64_t sm64_next(uint64_t *s) { |
|
890
|
|
|
|
|
|
|
uint64_t z = (*s += 0x9E3779B97F4A7C15ULL); |
|
891
|
|
|
|
|
|
|
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; |
|
892
|
|
|
|
|
|
|
z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; |
|
893
|
|
|
|
|
|
|
return z ^ (z >> 31); |
|
894
|
|
|
|
|
|
|
} |
|
895
|
|
|
|
|
|
|
|
|
896
|
|
|
|
|
|
|
static double sm64_drand(uint64_t *s) { |
|
897
|
|
|
|
|
|
|
return (double)(sm64_next(s) >> 11) * (1.0 / 9007199254740992.0); |
|
898
|
|
|
|
|
|
|
} |
|
899
|
|
|
|
|
|
|
|
|
900
|
|
|
|
|
|
|
static double _ts_randn(uint64_t *s) { |
|
901
|
|
|
|
|
|
|
double u1 = sm64_drand(s); |
|
902
|
|
|
|
|
|
|
double u2; |
|
903
|
|
|
|
|
|
|
if (u1 == 0.0) u1 = 1e-12; |
|
904
|
|
|
|
|
|
|
u2 = sm64_drand(s); |
|
905
|
|
|
|
|
|
|
return sqrt(-2.0 * log(u1)) * cos(6.283185307179586 * u2); |
|
906
|
|
|
|
|
|
|
} |
|
907
|
|
|
|
|
|
|
|
|
908
|
|
|
|
|
|
|
/* Thread-safe twin of _build_node_c: same split algorithm, but reads |
|
909
|
|
|
|
|
|
|
* randomness from a thread-private splitmix64 stream instead of |
|
910
|
|
|
|
|
|
|
* Drand01(), and writes into a TreeBuf instead of allocating Perl AVs |
|
911
|
|
|
|
|
|
|
* -- so it touches no interpreter-global state and is safe to call |
|
912
|
|
|
|
|
|
|
* concurrently from an OpenMP parallel region, one tree per thread. */ |
|
913
|
|
|
|
|
|
|
static int _build_node_packed(const double* x, int nf, int* idxs, int size, |
|
914
|
|
|
|
|
|
|
int depth, int limit, int mode_flag, |
|
915
|
|
|
|
|
|
|
int ext_active, TreeBuf *buf, uint64_t *rng) { |
|
916
|
|
|
|
|
|
|
double *lo, *hi; |
|
917
|
|
|
|
|
|
|
int *varying, nv, f, my_idx; |
|
918
|
|
|
|
|
|
|
|
|
919
|
|
|
|
|
|
|
if (depth >= limit || size <= 1) { |
|
920
|
|
|
|
|
|
|
my_idx = tb_push_node(buf, 0.0, (double)size, 0.0, 0.0, 0.0, 0.0); |
|
921
|
|
|
|
|
|
|
free(idxs); |
|
922
|
|
|
|
|
|
|
return my_idx; |
|
923
|
|
|
|
|
|
|
} |
|
924
|
|
|
|
|
|
|
|
|
925
|
|
|
|
|
|
|
lo = (double*)malloc(nf * sizeof(double)); |
|
926
|
|
|
|
|
|
|
hi = (double*)malloc(nf * sizeof(double)); |
|
927
|
|
|
|
|
|
|
for (f = 0; f < nf; f++) { |
|
928
|
|
|
|
|
|
|
lo[f] = HUGE_VAL; |
|
929
|
|
|
|
|
|
|
hi[f] = -HUGE_VAL; |
|
930
|
|
|
|
|
|
|
} |
|
931
|
|
|
|
|
|
|
for (int i = 0; i < size; i++) { |
|
932
|
|
|
|
|
|
|
const double* row = x + (size_t)idxs[i] * (size_t)nf; |
|
933
|
|
|
|
|
|
|
/* See the matching comment in _build_node_c: no isnan() guard |
|
934
|
|
|
|
|
|
|
* needed, since NaN < x / NaN > x are always false already -- |
|
935
|
|
|
|
|
|
|
* that's what lets this vectorize as a plain min/max scan. |
|
936
|
|
|
|
|
|
|
* omp simd here is thread-safe to call from inside the caller's |
|
937
|
|
|
|
|
|
|
* omp parallel region: it's a per-thread vectorization hint, |
|
938
|
|
|
|
|
|
|
* not a team construct, so it doesn't nest into anything. */ |
|
939
|
|
|
|
|
|
|
#ifdef _OPENMP |
|
940
|
|
|
|
|
|
|
#pragma omp simd |
|
941
|
|
|
|
|
|
|
#endif |
|
942
|
|
|
|
|
|
|
for (int f2 = 0; f2 < nf; f2++) { |
|
943
|
|
|
|
|
|
|
double v = row[f2]; |
|
944
|
|
|
|
|
|
|
if (v < lo[f2]) lo[f2] = v; |
|
945
|
|
|
|
|
|
|
if (v > hi[f2]) hi[f2] = v; |
|
946
|
|
|
|
|
|
|
} |
|
947
|
|
|
|
|
|
|
} |
|
948
|
|
|
|
|
|
|
|
|
949
|
|
|
|
|
|
|
varying = (int*)malloc(nf * sizeof(int)); |
|
950
|
|
|
|
|
|
|
nv = 0; |
|
951
|
|
|
|
|
|
|
for (f = 0; f < nf; f++) { |
|
952
|
|
|
|
|
|
|
if (lo[f] < hi[f]) varying[nv++] = f; |
|
953
|
|
|
|
|
|
|
} |
|
954
|
|
|
|
|
|
|
|
|
955
|
|
|
|
|
|
|
if (nv == 0) { |
|
956
|
|
|
|
|
|
|
free(lo); free(hi); free(varying); |
|
957
|
|
|
|
|
|
|
my_idx = tb_push_node(buf, 0.0, (double)size, 0.0, 0.0, 0.0, 0.0); |
|
958
|
|
|
|
|
|
|
free(idxs); |
|
959
|
|
|
|
|
|
|
return my_idx; |
|
960
|
|
|
|
|
|
|
} |
|
961
|
|
|
|
|
|
|
|
|
962
|
|
|
|
|
|
|
if (mode_flag == 0) { |
|
963
|
|
|
|
|
|
|
int attr = varying[(int)(sm64_drand(rng) * nv)]; |
|
964
|
|
|
|
|
|
|
double split = lo[attr] + sm64_drand(rng) * (hi[attr] - lo[attr]); |
|
965
|
|
|
|
|
|
|
int *lidx = (int*)malloc(size * sizeof(int)); |
|
966
|
|
|
|
|
|
|
int *ridx = (int*)malloc(size * sizeof(int)); |
|
967
|
|
|
|
|
|
|
int ln = 0, rn = 0, i, li, ri; |
|
968
|
|
|
|
|
|
|
|
|
969
|
|
|
|
|
|
|
for (i = 0; i < size; i++) { |
|
970
|
|
|
|
|
|
|
int row = idxs[i]; |
|
971
|
|
|
|
|
|
|
double v = x[(size_t)row * (size_t)nf + attr]; |
|
972
|
|
|
|
|
|
|
if (v < split) lidx[ln++] = row; else ridx[rn++] = row; |
|
973
|
|
|
|
|
|
|
} |
|
974
|
|
|
|
|
|
|
free(idxs); free(lo); free(hi); free(varying); |
|
975
|
|
|
|
|
|
|
|
|
976
|
|
|
|
|
|
|
li = _build_node_packed(x, nf, lidx, ln, depth + 1, limit, |
|
977
|
|
|
|
|
|
|
mode_flag, ext_active, buf, rng); |
|
978
|
|
|
|
|
|
|
ri = _build_node_packed(x, nf, ridx, rn, depth + 1, limit, |
|
979
|
|
|
|
|
|
|
mode_flag, ext_active, buf, rng); |
|
980
|
|
|
|
|
|
|
my_idx = tb_push_node(buf, 1.0, (double)attr, split, |
|
981
|
|
|
|
|
|
|
(double)li, (double)ri, 0.0); |
|
982
|
|
|
|
|
|
|
} else { |
|
983
|
|
|
|
|
|
|
int active = ext_active + 1; |
|
984
|
|
|
|
|
|
|
int *pool, *lidx, *ridx; |
|
985
|
|
|
|
|
|
|
double *coef; |
|
986
|
|
|
|
|
|
|
double b = 0.0; |
|
987
|
|
|
|
|
|
|
int ln = 0, rn = 0, i, k, li, ri, coff; |
|
988
|
|
|
|
|
|
|
|
|
989
|
|
|
|
|
|
|
if (active > nv) active = nv; |
|
990
|
|
|
|
|
|
|
pool = (int*)malloc(nv * sizeof(int)); |
|
991
|
|
|
|
|
|
|
memcpy(pool, varying, nv * sizeof(int)); |
|
992
|
|
|
|
|
|
|
for (i = 0; i < active; i++) { |
|
993
|
|
|
|
|
|
|
int j = i + (int)(sm64_drand(rng) * (nv - i)); |
|
994
|
|
|
|
|
|
|
int tmp = pool[i]; pool[i] = pool[j]; pool[j] = tmp; |
|
995
|
|
|
|
|
|
|
} |
|
996
|
|
|
|
|
|
|
|
|
997
|
|
|
|
|
|
|
coef = (double*)malloc(active * sizeof(double)); |
|
998
|
|
|
|
|
|
|
for (k = 0; k < active; k++) { |
|
999
|
|
|
|
|
|
|
int ff = pool[k]; |
|
1000
|
|
|
|
|
|
|
double c = _ts_randn(rng); |
|
1001
|
|
|
|
|
|
|
double p = lo[ff] + sm64_drand(rng) * (hi[ff] - lo[ff]); |
|
1002
|
|
|
|
|
|
|
coef[k] = c; |
|
1003
|
|
|
|
|
|
|
b += c * p; |
|
1004
|
|
|
|
|
|
|
} |
|
1005
|
|
|
|
|
|
|
|
|
1006
|
|
|
|
|
|
|
lidx = (int*)malloc(size * sizeof(int)); |
|
1007
|
|
|
|
|
|
|
ridx = (int*)malloc(size * sizeof(int)); |
|
1008
|
|
|
|
|
|
|
for (i = 0; i < size; i++) { |
|
1009
|
|
|
|
|
|
|
int row = idxs[i]; |
|
1010
|
|
|
|
|
|
|
double dot = 0.0; |
|
1011
|
|
|
|
|
|
|
for (k = 0; k < active; k++) { |
|
1012
|
|
|
|
|
|
|
dot += coef[k] * x[(size_t)row * (size_t)nf + pool[k]]; |
|
1013
|
|
|
|
|
|
|
} |
|
1014
|
|
|
|
|
|
|
if (dot <= b) lidx[ln++] = row; else ridx[rn++] = row; |
|
1015
|
|
|
|
|
|
|
} |
|
1016
|
|
|
|
|
|
|
free(idxs); free(lo); free(hi); free(varying); |
|
1017
|
|
|
|
|
|
|
|
|
1018
|
|
|
|
|
|
|
li = _build_node_packed(x, nf, lidx, ln, depth + 1, limit, |
|
1019
|
|
|
|
|
|
|
mode_flag, ext_active, buf, rng); |
|
1020
|
|
|
|
|
|
|
ri = _build_node_packed(x, nf, ridx, rn, depth + 1, limit, |
|
1021
|
|
|
|
|
|
|
mode_flag, ext_active, buf, rng); |
|
1022
|
|
|
|
|
|
|
coff = tb_push_coef(buf, pool, coef, active); |
|
1023
|
|
|
|
|
|
|
my_idx = tb_push_node(buf, 2.0, (double)coff, (double)active, |
|
1024
|
|
|
|
|
|
|
(double)li, (double)ri, b); |
|
1025
|
|
|
|
|
|
|
free(pool); free(coef); |
|
1026
|
|
|
|
|
|
|
} |
|
1027
|
|
|
|
|
|
|
return my_idx; |
|
1028
|
|
|
|
|
|
|
} |
|
1029
|
|
|
|
|
|
|
|
|
1030
|
|
|
|
|
|
|
void build_forest_openmp_xs(SV* x_sv, int n_pts, int n_feats, int n_trees, |
|
1031
|
|
|
|
|
|
|
int psi, int limit, int mode_flag, |
|
1032
|
|
|
|
|
|
|
int ext_level, SV* nodes_rv, SV* idx_rv, |
|
1033
|
|
|
|
|
|
|
SV* val_rv, int use_openmp) { |
|
1034
|
|
|
|
|
|
|
dTHX; |
|
1035
|
|
|
|
|
|
|
STRLEN tl; |
|
1036
|
|
|
|
|
|
|
const double* x; |
|
1037
|
|
|
|
|
|
|
AV *nodes_av, *idx_av, *val_av; |
|
1038
|
|
|
|
|
|
|
TreeBuf *bufs; |
|
1039
|
|
|
|
|
|
|
uint64_t base_seed; |
|
1040
|
|
|
|
|
|
|
int t; |
|
1041
|
|
|
|
|
|
|
|
|
1042
|
|
|
|
|
|
|
if (!SvROK(nodes_rv) || SvTYPE(SvRV(nodes_rv)) != SVt_PVAV || |
|
1043
|
|
|
|
|
|
|
!SvROK(idx_rv) || SvTYPE(SvRV(idx_rv)) != SVt_PVAV || |
|
1044
|
|
|
|
|
|
|
!SvROK(val_rv) || SvTYPE(SvRV(val_rv)) != SVt_PVAV) { |
|
1045
|
|
|
|
|
|
|
croak("build_forest_openmp_xs: nodes/idx/val must be arrayrefs"); |
|
1046
|
|
|
|
|
|
|
} |
|
1047
|
|
|
|
|
|
|
x = (const double*)SvPVbyte(x_sv, tl); |
|
1048
|
|
|
|
|
|
|
nodes_av = (AV*)SvRV(nodes_rv); |
|
1049
|
|
|
|
|
|
|
idx_av = (AV*)SvRV(idx_rv); |
|
1050
|
|
|
|
|
|
|
val_av = (AV*)SvRV(val_rv); |
|
1051
|
|
|
|
|
|
|
av_clear(nodes_av); av_clear(idx_av); av_clear(val_av); |
|
1052
|
|
|
|
|
|
|
if (n_trees > 0) { |
|
1053
|
|
|
|
|
|
|
av_extend(nodes_av, n_trees - 1); |
|
1054
|
|
|
|
|
|
|
av_extend(idx_av, n_trees - 1); |
|
1055
|
|
|
|
|
|
|
av_extend(val_av, n_trees - 1); |
|
1056
|
|
|
|
|
|
|
} |
|
1057
|
|
|
|
|
|
|
|
|
1058
|
|
|
|
|
|
|
/* Single Drand01() call, before the parallel region starts, so it's |
|
1059
|
|
|
|
|
|
|
* still a plain serial call into the interpreter's RNG state. */ |
|
1060
|
|
|
|
|
|
|
base_seed = (uint64_t)(Drand01() * 18446744073709551615.0); |
|
1061
|
|
|
|
|
|
|
|
|
1062
|
|
|
|
|
|
|
bufs = (TreeBuf*)malloc((size_t)n_trees * sizeof(TreeBuf)); |
|
1063
|
|
|
|
|
|
|
for (t = 0; t < n_trees; t++) tb_init(&bufs[t]); |
|
1064
|
|
|
|
|
|
|
|
|
1065
|
|
|
|
|
|
|
#ifdef _OPENMP |
|
1066
|
|
|
|
|
|
|
#pragma omp parallel for schedule(dynamic) if(use_openmp) |
|
1067
|
|
|
|
|
|
|
#endif |
|
1068
|
|
|
|
|
|
|
for (int t = 0; t < n_trees; t++) { |
|
1069
|
|
|
|
|
|
|
/* Seeded from the tree index, not thread id or iteration order, |
|
1070
|
|
|
|
|
|
|
* so the mapping from tree -> RNG stream is independent of |
|
1071
|
|
|
|
|
|
|
* OMP_NUM_THREADS / scheduling. sm64_next() mixes once more so |
|
1072
|
|
|
|
|
|
|
* adjacent tree indices (which differ by one golden-ratio step) |
|
1073
|
|
|
|
|
|
|
* don't start from too-similar states. */ |
|
1074
|
|
|
|
|
|
|
uint64_t rng = base_seed + (uint64_t)t * 0x9E3779B97F4A7C15ULL; |
|
1075
|
|
|
|
|
|
|
rng = sm64_next(&rng); |
|
1076
|
|
|
|
|
|
|
int *all = (int*)malloc((size_t)n_pts * sizeof(int)); |
|
1077
|
|
|
|
|
|
|
int *sample; |
|
1078
|
|
|
|
|
|
|
int i; |
|
1079
|
|
|
|
|
|
|
|
|
1080
|
|
|
|
|
|
|
for (i = 0; i < n_pts; i++) all[i] = i; |
|
1081
|
|
|
|
|
|
|
for (i = 0; i < psi; i++) { |
|
1082
|
|
|
|
|
|
|
int j = i + (int)(sm64_drand(&rng) * (n_pts - i)); |
|
1083
|
|
|
|
|
|
|
int tmp = all[i]; all[i] = all[j]; all[j] = tmp; |
|
1084
|
|
|
|
|
|
|
} |
|
1085
|
|
|
|
|
|
|
sample = (int*)malloc((size_t)psi * sizeof(int)); |
|
1086
|
|
|
|
|
|
|
memcpy(sample, all, (size_t)psi * sizeof(int)); |
|
1087
|
|
|
|
|
|
|
free(all); |
|
1088
|
|
|
|
|
|
|
|
|
1089
|
|
|
|
|
|
|
_build_node_packed(x, n_feats, sample, psi, 0, limit, mode_flag, |
|
1090
|
|
|
|
|
|
|
ext_level, &bufs[t], &rng); |
|
1091
|
|
|
|
|
|
|
} |
|
1092
|
|
|
|
|
|
|
|
|
1093
|
|
|
|
|
|
|
for (t = 0; t < n_trees; t++) { |
|
1094
|
|
|
|
|
|
|
/* newSVpvn(NULL, 0) makes an undef SV, not an empty-string one -- |
|
1095
|
|
|
|
|
|
|
* axis-mode trees never call tb_push_coef, so idx/val stay NULL. |
|
1096
|
|
|
|
|
|
|
* Pass "" instead so the Perl side's unpack('...', $sv) always |
|
1097
|
|
|
|
|
|
|
* gets a defined (if empty) string, never undef. */ |
|
1098
|
|
|
|
|
|
|
av_store(nodes_av, t, newSVpvn((char*)bufs[t].nodes, |
|
1099
|
|
|
|
|
|
|
bufs[t].n_nodes * 6 * sizeof(double))); |
|
1100
|
|
|
|
|
|
|
av_store(idx_av, t, bufs[t].n_idx |
|
1101
|
|
|
|
|
|
|
? newSVpvn((char*)bufs[t].idx, bufs[t].n_idx * sizeof(int)) |
|
1102
|
|
|
|
|
|
|
: newSVpvn("", 0)); |
|
1103
|
|
|
|
|
|
|
av_store(val_av, t, bufs[t].n_val |
|
1104
|
|
|
|
|
|
|
? newSVpvn((char*)bufs[t].val, bufs[t].n_val * sizeof(double)) |
|
1105
|
|
|
|
|
|
|
: newSVpvn("", 0)); |
|
1106
|
|
|
|
|
|
|
tb_free(&bufs[t]); |
|
1107
|
|
|
|
|
|
|
} |
|
1108
|
|
|
|
|
|
|
free(bufs); |
|
1109
|
|
|
|
|
|
|
} |
|
1110
|
|
|
|
|
|
|
|
|
1111
|
|
|
|
|
|
|
/* --------------------------------------------------------------------- |
|
1112
|
|
|
|
|
|
|
* impute_fill_xs(data_sv, n_pts, n_feats, how, out_rv) |
|
1113
|
|
|
|
|
|
|
* |
|
1114
|
|
|
|
|
|
|
* C replacement for _compute_impute_fill's Perl loop: walks the raw |
|
1115
|
|
|
|
|
|
|
* arrayref-of-arrayrefs directly (like pack_input_xs), collecting each |
|
1116
|
|
|
|
|
|
|
* feature's present (defined) values, then reduces them to one fill |
|
1117
|
|
|
|
|
|
|
* value per feature -- mean (how == 0) or median (how == 1) -- and |
|
1118
|
|
|
|
|
|
|
* writes n_feats doubles into out_rv. |
|
1119
|
|
|
|
|
|
|
* |
|
1120
|
|
|
|
|
|
|
* Values are collected in row order (i = 0..n_pts-1), the same order |
|
1121
|
|
|
|
|
|
|
* the Perl version's `grep { defined } map { $_->[$f] } @data` walks |
|
1122
|
|
|
|
|
|
|
* them in, so the mean's left-to-right summation lands on the exact |
|
1123
|
|
|
|
|
|
|
* same float as the Perl path -- use_c toggles speed here, not the |
|
1124
|
|
|
|
|
|
|
* computed fill, matching the rest of the module. |
|
1125
|
|
|
|
|
|
|
* |
|
1126
|
|
|
|
|
|
|
* The median is an exact order statistic (not summation-dependent), so |
|
1127
|
|
|
|
|
|
|
* it matches the Perl path's sort-based median by definition regardless |
|
1128
|
|
|
|
|
|
|
* of which selection algorithm finds it. Croaks with the same message |
|
1129
|
|
|
|
|
|
|
* as the Perl fallback if a feature has no present values anywhere in |
|
1130
|
|
|
|
|
|
|
* the dataset. */ |
|
1131
|
|
|
|
|
|
|
typedef struct { double *v; size_t n, cap; } DVec; |
|
1132
|
|
|
|
|
|
|
|
|
1133
|
|
|
|
|
|
|
static void dvec_push(DVec *d, double x) { |
|
1134
|
|
|
|
|
|
|
if (d->n == d->cap) { |
|
1135
|
|
|
|
|
|
|
size_t newcap = d->cap ? d->cap * 2 : 64; |
|
1136
|
|
|
|
|
|
|
d->v = (double*)realloc(d->v, newcap * sizeof(double)); |
|
1137
|
|
|
|
|
|
|
d->cap = newcap; |
|
1138
|
|
|
|
|
|
|
} |
|
1139
|
|
|
|
|
|
|
d->v[d->n++] = x; |
|
1140
|
|
|
|
|
|
|
} |
|
1141
|
|
|
|
|
|
|
|
|
1142
|
|
|
|
|
|
|
static void _dswap(double *a, double *b) { double t = *a; *a = *b; *b = t; } |
|
1143
|
|
|
|
|
|
|
|
|
1144
|
|
|
|
|
|
|
/* Lomuto partition with a median-of-three pivot (avoids the O(n^2) |
|
1145
|
|
|
|
|
|
|
* worst case a fixed pivot hits on already-sorted or reverse-sorted |
|
1146
|
|
|
|
|
|
|
* input, which real feature columns -- timestamps, counters -- often |
|
1147
|
|
|
|
|
|
|
* are). Returns the pivot's final index. */ |
|
1148
|
|
|
|
|
|
|
static int _partition_lomuto(double *a, int lo, int hi) { |
|
1149
|
|
|
|
|
|
|
int mid = lo + (hi - lo) / 2; |
|
1150
|
|
|
|
|
|
|
double pivot; |
|
1151
|
|
|
|
|
|
|
int i, j; |
|
1152
|
|
|
|
|
|
|
if (a[mid] < a[lo]) _dswap(&a[lo], &a[mid]); |
|
1153
|
|
|
|
|
|
|
if (a[hi] < a[lo]) _dswap(&a[lo], &a[hi]); |
|
1154
|
|
|
|
|
|
|
if (a[hi] < a[mid]) _dswap(&a[mid], &a[hi]); |
|
1155
|
|
|
|
|
|
|
_dswap(&a[mid], &a[hi]); |
|
1156
|
|
|
|
|
|
|
pivot = a[hi]; |
|
1157
|
|
|
|
|
|
|
i = lo; |
|
1158
|
|
|
|
|
|
|
for (j = lo; j < hi; j++) { |
|
1159
|
|
|
|
|
|
|
if (a[j] < pivot) { _dswap(&a[i], &a[j]); i++; } |
|
1160
|
|
|
|
|
|
|
} |
|
1161
|
|
|
|
|
|
|
_dswap(&a[i], &a[hi]); |
|
1162
|
|
|
|
|
|
|
return i; |
|
1163
|
|
|
|
|
|
|
} |
|
1164
|
|
|
|
|
|
|
|
|
1165
|
|
|
|
|
|
|
/* Quickselect: returns the k-th smallest (0-indexed) of a[0..n-1], |
|
1166
|
|
|
|
|
|
|
* reordering a[] in the process (fine -- it's a private scratch copy). |
|
1167
|
|
|
|
|
|
|
* O(n) average case vs. a full O(n log n) sort. */ |
|
1168
|
|
|
|
|
|
|
static double _kth_smallest(double *a, int n, int k) { |
|
1169
|
|
|
|
|
|
|
int lo = 0, hi = n - 1; |
|
1170
|
|
|
|
|
|
|
while (lo < hi) { |
|
1171
|
|
|
|
|
|
|
int p = _partition_lomuto(a, lo, hi); |
|
1172
|
|
|
|
|
|
|
if (p == k) return a[p]; |
|
1173
|
|
|
|
|
|
|
if (p < k) lo = p + 1; else hi = p - 1; |
|
1174
|
|
|
|
|
|
|
} |
|
1175
|
|
|
|
|
|
|
return a[lo]; |
|
1176
|
|
|
|
|
|
|
} |
|
1177
|
|
|
|
|
|
|
|
|
1178
|
|
|
|
|
|
|
/* Median of a[0..n-1] (reorders a[]). Odd n: the single middle order |
|
1179
|
|
|
|
|
|
|
* statistic. Even n: quickselect finds the lower-median at k = n/2-1, |
|
1180
|
|
|
|
|
|
|
* which leaves every a[i > k] >= a[k] (the standard quickselect |
|
1181
|
|
|
|
|
|
|
* post-condition) -- so the upper-median is just the min of that |
|
1182
|
|
|
|
|
|
|
* remaining slice, one more linear scan instead of a second full |
|
1183
|
|
|
|
|
|
|
* selection pass. */ |
|
1184
|
|
|
|
|
|
|
static double _median_select(double *a, int n) { |
|
1185
|
|
|
|
|
|
|
if (n % 2 == 1) { |
|
1186
|
|
|
|
|
|
|
return _kth_smallest(a, n, n / 2); |
|
1187
|
|
|
|
|
|
|
} else { |
|
1188
|
|
|
|
|
|
|
int k = n / 2 - 1; |
|
1189
|
|
|
|
|
|
|
double lower = _kth_smallest(a, n, k); |
|
1190
|
|
|
|
|
|
|
double upper = a[k + 1]; |
|
1191
|
|
|
|
|
|
|
int i; |
|
1192
|
|
|
|
|
|
|
for (i = k + 2; i < n; i++) { |
|
1193
|
|
|
|
|
|
|
if (a[i] < upper) upper = a[i]; |
|
1194
|
|
|
|
|
|
|
} |
|
1195
|
|
|
|
|
|
|
return (lower + upper) / 2.0; |
|
1196
|
|
|
|
|
|
|
} |
|
1197
|
|
|
|
|
|
|
} |
|
1198
|
|
|
|
|
|
|
|
|
1199
|
|
|
|
|
|
|
void impute_fill_xs(SV* data_sv, int n_pts, int n_feats, int how, |
|
1200
|
|
|
|
|
|
|
SV* out_rv) { |
|
1201
|
|
|
|
|
|
|
dTHX; |
|
1202
|
|
|
|
|
|
|
AV *outer, *out; |
|
1203
|
|
|
|
|
|
|
DVec *cols; |
|
1204
|
|
|
|
|
|
|
int i, f; |
|
1205
|
|
|
|
|
|
|
|
|
1206
|
|
|
|
|
|
|
if (!SvROK(data_sv) || SvTYPE(SvRV(data_sv)) != SVt_PVAV) { |
|
1207
|
|
|
|
|
|
|
croak("impute_fill_xs: data must be an arrayref"); |
|
1208
|
|
|
|
|
|
|
} |
|
1209
|
|
|
|
|
|
|
if (!SvROK(out_rv) || SvTYPE(SvRV(out_rv)) != SVt_PVAV) { |
|
1210
|
|
|
|
|
|
|
croak("impute_fill_xs: out must be an arrayref"); |
|
1211
|
|
|
|
|
|
|
} |
|
1212
|
|
|
|
|
|
|
outer = (AV*)SvRV(data_sv); |
|
1213
|
|
|
|
|
|
|
out = (AV*)SvRV(out_rv); |
|
1214
|
|
|
|
|
|
|
|
|
1215
|
|
|
|
|
|
|
cols = (DVec*)calloc((size_t)n_feats, sizeof(DVec)); |
|
1216
|
|
|
|
|
|
|
|
|
1217
|
|
|
|
|
|
|
for (i = 0; i < n_pts; i++) { |
|
1218
|
|
|
|
|
|
|
SV** row_pp = av_fetch(outer, i, 0); |
|
1219
|
|
|
|
|
|
|
AV* row; |
|
1220
|
|
|
|
|
|
|
if (!row_pp || !*row_pp || !SvROK(*row_pp) || |
|
1221
|
|
|
|
|
|
|
SvTYPE(SvRV(*row_pp)) != SVt_PVAV) { |
|
1222
|
|
|
|
|
|
|
continue; |
|
1223
|
|
|
|
|
|
|
} |
|
1224
|
|
|
|
|
|
|
row = (AV*)SvRV(*row_pp); |
|
1225
|
|
|
|
|
|
|
for (f = 0; f < n_feats; f++) { |
|
1226
|
|
|
|
|
|
|
SV** v = av_fetch(row, f, 0); |
|
1227
|
|
|
|
|
|
|
if (v && *v && SvOK(*v)) { |
|
1228
|
|
|
|
|
|
|
dvec_push(&cols[f], SvNV(*v)); |
|
1229
|
|
|
|
|
|
|
} |
|
1230
|
|
|
|
|
|
|
} |
|
1231
|
|
|
|
|
|
|
} |
|
1232
|
|
|
|
|
|
|
|
|
1233
|
|
|
|
|
|
|
/* Validate every column before freeing anything: croak() longjmps |
|
1234
|
|
|
|
|
|
|
* out of this function, so any cleanup loop reachable after a |
|
1235
|
|
|
|
|
|
|
* partial computation has already started (and already freed some |
|
1236
|
|
|
|
|
|
|
* cols[i].v) risks a double free on those same pointers. Checking |
|
1237
|
|
|
|
|
|
|
* all columns up front, before the computation loop below frees |
|
1238
|
|
|
|
|
|
|
* anything, avoids that entirely. Matches the Perl fallback's |
|
1239
|
|
|
|
|
|
|
* behaviour of reporting the first empty column in feature order. */ |
|
1240
|
|
|
|
|
|
|
for (f = 0; f < n_feats; f++) { |
|
1241
|
|
|
|
|
|
|
if (cols[f].n == 0) { |
|
1242
|
|
|
|
|
|
|
int col = f; |
|
1243
|
|
|
|
|
|
|
for (i = 0; i < n_feats; i++) free(cols[i].v); |
|
1244
|
|
|
|
|
|
|
free(cols); |
|
1245
|
|
|
|
|
|
|
croak("impute: feature column %d has no present values", col); |
|
1246
|
|
|
|
|
|
|
} |
|
1247
|
|
|
|
|
|
|
} |
|
1248
|
|
|
|
|
|
|
|
|
1249
|
|
|
|
|
|
|
av_clear(out); |
|
1250
|
|
|
|
|
|
|
if (n_feats > 0) av_extend(out, n_feats - 1); |
|
1251
|
|
|
|
|
|
|
|
|
1252
|
|
|
|
|
|
|
for (f = 0; f < n_feats; f++) { |
|
1253
|
|
|
|
|
|
|
double result; |
|
1254
|
|
|
|
|
|
|
if (how == 0) { |
|
1255
|
|
|
|
|
|
|
double sum = 0.0; |
|
1256
|
|
|
|
|
|
|
for (i = 0; i < (int)cols[f].n; i++) sum += cols[f].v[i]; |
|
1257
|
|
|
|
|
|
|
result = sum / (double)cols[f].n; |
|
1258
|
|
|
|
|
|
|
} else { |
|
1259
|
|
|
|
|
|
|
result = _median_select(cols[f].v, (int)cols[f].n); |
|
1260
|
|
|
|
|
|
|
} |
|
1261
|
|
|
|
|
|
|
av_store(out, f, newSVnv(result)); |
|
1262
|
|
|
|
|
|
|
free(cols[f].v); |
|
1263
|
|
|
|
|
|
|
} |
|
1264
|
|
|
|
|
|
|
free(cols); |
|
1265
|
|
|
|
|
|
|
} |
|
1266
|
|
|
|
|
|
|
__INLINE_C__ |
|
1267
|
|
|
|
|
|
|
|
|
1268
|
|
|
|
|
|
|
# IF_NO_C=1 skips even attempting to set up the C backend -- useful for |
|
1269
|
|
|
|
|
|
|
# forcing the pure-Perl path without touching every constructor call |
|
1270
|
|
|
|
|
|
|
# (use_c => 0), e.g. to get a clean timing baseline or to avoid the |
|
1271
|
|
|
|
|
|
|
# compile attempt's overhead/noise in a container known to lack a |
|
1272
|
|
|
|
|
|
|
# compiler. Everything below is skipped and $HAS_C stays 0. |
|
1273
|
|
|
|
|
|
|
unless ( $ENV{IF_NO_C} ) { |
|
1274
|
|
|
|
|
|
|
|
|
1275
|
|
|
|
|
|
|
# Defaults recorded when `perl Makefile.PL` ran. Makefile.PL generates |
|
1276
|
|
|
|
|
|
|
# Algorithm::Classifier::IsolationForest::BuildFlags, capturing the |
|
1277
|
|
|
|
|
|
|
# IF_* values active at configure time plus whether a prebuilt object |
|
1278
|
|
|
|
|
|
|
# was scheduled for install (see "Compile at install time" in the POD |
|
1279
|
|
|
|
|
|
|
# below). From a plain source checkout the generated file is absent, |
|
1280
|
|
|
|
|
|
|
# the hard defaults here apply, and no prebuilt object is looked for. |
|
1281
|
|
|
|
|
|
|
my ( $def_opt, $def_arch, $def_no_omp, $prebuilt ) = ( '-O3', '', 0, 0 ); |
|
1282
|
|
|
|
|
|
|
{ |
|
1283
|
|
|
|
|
|
|
local $@; |
|
1284
|
|
|
|
|
|
|
my $rec = eval { |
|
1285
|
|
|
|
|
|
|
require Algorithm::Classifier::IsolationForest::BuildFlags; |
|
1286
|
|
|
|
|
|
|
Algorithm::Classifier::IsolationForest::BuildFlags::flags(); |
|
1287
|
|
|
|
|
|
|
}; |
|
1288
|
|
|
|
|
|
|
if ( ref $rec eq 'HASH' ) { |
|
1289
|
|
|
|
|
|
|
$def_opt = $rec->{opt} if defined $rec->{opt}; |
|
1290
|
|
|
|
|
|
|
$def_arch = $rec->{arch} if defined $rec->{arch}; |
|
1291
|
|
|
|
|
|
|
$def_no_omp = $rec->{no_openmp} ? 1 : 0; |
|
1292
|
|
|
|
|
|
|
$prebuilt = $rec->{prebuilt} ? 1 : 0; |
|
1293
|
|
|
|
|
|
|
} |
|
1294
|
|
|
|
|
|
|
} |
|
1295
|
|
|
|
|
|
|
|
|
1296
|
|
|
|
|
|
|
# -O3 is the usual default: it's safe to enable unconditionally and |
|
1297
|
|
|
|
|
|
|
# matters here -- the extended-mode oblique dot product is wrapped in |
|
1298
|
|
|
|
|
|
|
# `#pragma omp simd`, but without aggressive optimization the compiler |
|
1299
|
|
|
|
|
|
|
# may still emit scalar code. Use OPTIMIZE (not CCFLAGS) -- CCFLAGS is |
|
1300
|
|
|
|
|
|
|
# prepended to the cc line and would be shadowed by Perl's own `-O2 -g` |
|
1301
|
|
|
|
|
|
|
# that ExtUtils::MakeMaker appends afterward (last `-O` wins in gcc). |
|
1302
|
|
|
|
|
|
|
# IF_OPT overrides the level itself (e.g. IF_OPT=-O2 to work around a |
|
1303
|
|
|
|
|
|
|
# miscompile, or to shorten build time while developing); it's |
|
1304
|
|
|
|
|
|
|
# validated against a fixed set of GCC/Clang -O flags rather than |
|
1305
|
|
|
|
|
|
|
# interpolated as-is, since this string eventually reaches a shell |
|
1306
|
|
|
|
|
|
|
# command line via ExtUtils::MakeMaker. |
|
1307
|
|
|
|
|
|
|
my $opt = $def_opt; |
|
1308
|
|
|
|
|
|
|
if ( defined $ENV{IF_OPT} ) { |
|
1309
|
|
|
|
|
|
|
if ( $ENV{IF_OPT} =~ /\A-O[0123sgz]\z/ ) { |
|
1310
|
|
|
|
|
|
|
$opt = $ENV{IF_OPT}; |
|
1311
|
|
|
|
|
|
|
} else { |
|
1312
|
|
|
|
|
|
|
warn "Algorithm::Classifier::IsolationForest: ignoring invalid " |
|
1313
|
|
|
|
|
|
|
. "IF_OPT value '$ENV{IF_OPT}' (expected one of -O0 -O1 -O2 " |
|
1314
|
|
|
|
|
|
|
. "-O3 -Os -Og -Oz); using $opt\n"; |
|
1315
|
|
|
|
|
|
|
} |
|
1316
|
|
|
|
|
|
|
} |
|
1317
|
|
|
|
|
|
|
|
|
1318
|
|
|
|
|
|
|
# -march= lets the compiler target specific instruction-set |
|
1319
|
|
|
|
|
|
|
# extensions (AVX2 gather + FMA, etc.) for the oblique dot product |
|
1320
|
|
|
|
|
|
|
# and the fit-time min/max scan's `#pragma omp simd` loops. |
|
1321
|
|
|
|
|
|
|
# |
|
1322
|
|
|
|
|
|
|
# IF_ARCH= sets it explicitly (e.g. "x86-64-v3", "skylake", |
|
1323
|
|
|
|
|
|
|
# "znver3") -- validated against a conservative identifier charset |
|
1324
|
|
|
|
|
|
|
# since, like IF_OPT, it flows into a compiler command line. |
|
1325
|
|
|
|
|
|
|
# IF_NATIVE=1 remains as shorthand for IF_ARCH=native and is used |
|
1326
|
|
|
|
|
|
|
# when IF_ARCH isn't set. Prefer a specific IF_ARCH value over |
|
1327
|
|
|
|
|
|
|
# IF_NATIVE on a machine you don't control exclusively: blanket |
|
1328
|
|
|
|
|
|
|
# -march=native pulls in whatever the build host has, including |
|
1329
|
|
|
|
|
|
|
# AVX-512 on some Intel CPUs, which is known to trigger clock |
|
1330
|
|
|
|
|
|
|
# throttling under sustained heavy use and can make throughput |
|
1331
|
|
|
|
|
|
|
# *worse* than a conservative target like x86-64-v3 (AVX2, no |
|
1332
|
|
|
|
|
|
|
# AVX-512). Either way, the cached artefact under _Inline/ is then |
|
1333
|
|
|
|
|
|
|
# pinned to that instruction set, so leave both unset if the |
|
1334
|
|
|
|
|
|
|
# directory is shared across machines with different CPUs. |
|
1335
|
|
|
|
|
|
|
my $arch = $def_arch; |
|
1336
|
|
|
|
|
|
|
if ( defined $ENV{IF_ARCH} ) { |
|
1337
|
|
|
|
|
|
|
if ( $ENV{IF_ARCH} eq '' or $ENV{IF_ARCH} eq 'none' ) { |
|
1338
|
|
|
|
|
|
|
|
|
1339
|
|
|
|
|
|
|
# Explicit opt-out: overrides an arch recorded at configure |
|
1340
|
|
|
|
|
|
|
# time (there is no other way to request a plain build on |
|
1341
|
|
|
|
|
|
|
# an install configured with IF_ARCH). |
|
1342
|
|
|
|
|
|
|
$arch = ''; |
|
1343
|
|
|
|
|
|
|
} elsif ( $ENV{IF_ARCH} =~ /\A[A-Za-z0-9_.+=-]+\z/ ) { |
|
1344
|
|
|
|
|
|
|
$arch = $ENV{IF_ARCH}; |
|
1345
|
|
|
|
|
|
|
} else { |
|
1346
|
|
|
|
|
|
|
warn "Algorithm::Classifier::IsolationForest: ignoring invalid " . "IF_ARCH value '$ENV{IF_ARCH}'\n"; |
|
1347
|
|
|
|
|
|
|
} |
|
1348
|
|
|
|
|
|
|
} elsif ( $ENV{IF_NATIVE} ) { |
|
1349
|
|
|
|
|
|
|
$arch = 'native'; |
|
1350
|
|
|
|
|
|
|
} |
|
1351
|
|
|
|
|
|
|
# -ffp-contract=off rides along with any -march: once the target |
|
1352
|
|
|
|
|
|
|
# has FMA (x86-64-v3, most -march=native hosts), the compiler may |
|
1353
|
|
|
|
|
|
|
# otherwise contract a*b+c expressions into fused multiply-adds |
|
1354
|
|
|
|
|
|
|
# whose different rounding breaks the documented guarantee that |
|
1355
|
|
|
|
|
|
|
# use_c => 1 and use_c => 0 build bit-identical trees (one ulp in a |
|
1356
|
|
|
|
|
|
|
# split value cascades into a structurally different tree). The |
|
1357
|
|
|
|
|
|
|
# -march speedup comes from AVX2 vectorization, not contraction, |
|
1358
|
|
|
|
|
|
|
# so this costs little (verified against the fit-determinism and |
|
1359
|
|
|
|
|
|
|
# scoring-parity tests). |
|
1360
|
|
|
|
|
|
|
my $opt_level = $opt; |
|
1361
|
|
|
|
|
|
|
$opt_level .= " -march=$arch -ffp-contract=off" if length $arch; |
|
1362
|
|
|
|
|
|
|
|
|
1363
|
|
|
|
|
|
|
# IF_NO_OPENMP=1 forces the serial C build: the OpenMP compile attempt |
|
1364
|
|
|
|
|
|
|
# is skipped, so the object has no libgomp linkage and never starts an |
|
1365
|
|
|
|
|
|
|
# OpenMP runtime in the process. Distinct from OMP_NUM_THREADS=1, |
|
1366
|
|
|
|
|
|
|
# which runs the parallel code on a single thread but still loads |
|
1367
|
|
|
|
|
|
|
# libgomp. An explicit IF_NO_OPENMP=0 re-enables OpenMP over a |
|
1368
|
|
|
|
|
|
|
# no-openmp configure-time default. |
|
1369
|
|
|
|
|
|
|
my $no_omp |
|
1370
|
|
|
|
|
|
|
= defined $ENV{IF_NO_OPENMP} |
|
1371
|
|
|
|
|
|
|
? ( $ENV{IF_NO_OPENMP} ? 1 : 0 ) |
|
1372
|
|
|
|
|
|
|
: $def_no_omp; |
|
1373
|
|
|
|
|
|
|
|
|
1374
|
|
|
|
|
|
|
# The prebuilt object is only trusted when the effective flags match |
|
1375
|
|
|
|
|
|
|
# what it was compiled with; any difference -- or an explicit |
|
1376
|
|
|
|
|
|
|
# IF_RUNTIME_BUILD=1 -- falls through to the classic runtime Inline::C |
|
1377
|
|
|
|
|
|
|
# build below, which honours the requested flags via the MD5-keyed |
|
1378
|
|
|
|
|
|
|
# _Inline/ cache exactly as before prebuilt support existed. |
|
1379
|
|
|
|
|
|
|
# IF_INSTALL_BUILD is the `make` rule driving the install-time compile |
|
1380
|
|
|
|
|
|
|
# (see Makefile.PL); it must never short-circuit into loading an |
|
1381
|
|
|
|
|
|
|
# older object. |
|
1382
|
|
|
|
|
|
|
my $use_prebuilt |
|
1383
|
|
|
|
|
|
|
= $prebuilt |
|
1384
|
|
|
|
|
|
|
&& !$ENV{IF_RUNTIME_BUILD} |
|
1385
|
|
|
|
|
|
|
&& !$ENV{IF_INSTALL_BUILD} |
|
1386
|
|
|
|
|
|
|
&& $opt eq $def_opt |
|
1387
|
|
|
|
|
|
|
&& $arch eq $def_arch |
|
1388
|
|
|
|
|
|
|
&& $no_omp == $def_no_omp; |
|
1389
|
|
|
|
|
|
|
|
|
1390
|
|
|
|
|
|
|
# Inline::C hashes the C source to decide whether to rebuild but |
|
1391
|
|
|
|
|
|
|
# does NOT include CCFLAGS / OPTIMIZE in that hash. Without the |
|
1392
|
|
|
|
|
|
|
# tag below, toggling IF_NATIVE/IF_ARCH/IF_OPT (or editing the |
|
1393
|
|
|
|
|
|
|
# optimisation flags here) would silently reuse a cached binary |
|
1394
|
|
|
|
|
|
|
# built with stale flags. Embedding the active flags as a leading |
|
1395
|
|
|
|
|
|
|
# comment forces the hash to differ when they change. The OpenMP |
|
1396
|
|
|
|
|
|
|
# and serial builds get distinct tags so they cache to separate |
|
1397
|
|
|
|
|
|
|
# artefacts. |
|
1398
|
|
|
|
|
|
|
my $omp_tag = "/* if_build: openmp $opt_level */\n"; |
|
1399
|
|
|
|
|
|
|
my $serial_tag = "/* if_build: serial $opt_level */\n"; |
|
1400
|
|
|
|
|
|
|
|
|
1401
|
|
|
|
|
|
|
if ( $ENV{IF_INSTALL_BUILD} ) { |
|
1402
|
|
|
|
|
|
|
|
|
1403
|
|
|
|
|
|
|
# `make` is driving: the rule Makefile.PL appended runs this load |
|
1404
|
|
|
|
|
|
|
# with IF_INSTALL_BUILD=1 and @ARGV = (version, INST_ARCHLIB), |
|
1405
|
|
|
|
|
|
|
# which is where Inline's install mode reads them from. _INSTALL_ |
|
1406
|
|
|
|
|
|
|
# makes Inline compile the backend and place the shared object |
|
1407
|
|
|
|
|
|
|
# under blib/arch so `make install` ships it; NAME/VERSION give |
|
1408
|
|
|
|
|
|
|
# the object a fixed identity XSLoader can find at run time |
|
1409
|
|
|
|
|
|
|
# (Inline's install mode also requires both and checks VERSION |
|
1410
|
|
|
|
|
|
|
# against $ARGV[0]). Same OpenMP-then-serial fallback as the |
|
1411
|
|
|
|
|
|
|
# runtime build below. |
|
1412
|
|
|
|
|
|
|
my @install = ( |
|
1413
|
|
|
|
|
|
|
NAME => __PACKAGE__, |
|
1414
|
|
|
|
|
|
|
VERSION => $VERSION, |
|
1415
|
|
|
|
|
|
|
_INSTALL_ => 1, |
|
1416
|
|
|
|
|
|
|
); |
|
1417
|
|
|
|
|
|
|
unless ($no_omp) { |
|
1418
|
|
|
|
|
|
|
local $@; |
|
1419
|
|
|
|
|
|
|
eval { |
|
1420
|
|
|
|
|
|
|
require Inline; |
|
1421
|
|
|
|
|
|
|
Inline->import( |
|
1422
|
|
|
|
|
|
|
C => $omp_tag . $C_CODE, |
|
1423
|
|
|
|
|
|
|
CCFLAGS => '-fopenmp', |
|
1424
|
|
|
|
|
|
|
OPTIMIZE => $opt_level, |
|
1425
|
|
|
|
|
|
|
LIBS => '-lm -lgomp', |
|
1426
|
|
|
|
|
|
|
@install, |
|
1427
|
|
|
|
|
|
|
); |
|
1428
|
|
|
|
|
|
|
$HAS_C = 1; |
|
1429
|
|
|
|
|
|
|
}; |
|
1430
|
|
|
|
|
|
|
} ## end unless ($no_omp) |
|
1431
|
|
|
|
|
|
|
unless ($HAS_C) { |
|
1432
|
|
|
|
|
|
|
local $@; |
|
1433
|
|
|
|
|
|
|
eval { |
|
1434
|
|
|
|
|
|
|
require Inline; |
|
1435
|
|
|
|
|
|
|
Inline->import( |
|
1436
|
|
|
|
|
|
|
C => $serial_tag . $C_CODE, |
|
1437
|
|
|
|
|
|
|
OPTIMIZE => $opt_level, |
|
1438
|
|
|
|
|
|
|
LIBS => '-lm', |
|
1439
|
|
|
|
|
|
|
@install, |
|
1440
|
|
|
|
|
|
|
); |
|
1441
|
|
|
|
|
|
|
$HAS_C = 1; |
|
1442
|
|
|
|
|
|
|
}; |
|
1443
|
|
|
|
|
|
|
} ## end unless ($HAS_C) |
|
1444
|
|
|
|
|
|
|
$C_SOURCE = 'prebuilt' if $HAS_C; |
|
1445
|
|
|
|
|
|
|
} else { |
|
1446
|
|
|
|
|
|
|
|
|
1447
|
|
|
|
|
|
|
# Fast path: the object compiled at `make` time was installed |
|
1448
|
|
|
|
|
|
|
# under auto/ like any XS module, so plain XSLoader digs it out of |
|
1449
|
|
|
|
|
|
|
# @INC with no Inline involvement -- no compiler, no _Inline/ |
|
1450
|
|
|
|
|
|
|
# directory, and a few ms instead of a first-run compile. Any |
|
1451
|
|
|
|
|
|
|
# failure (object deleted, different perl, version mismatch after |
|
1452
|
|
|
|
|
|
|
# an upgrade, libgomp since removed) just falls through to the |
|
1453
|
|
|
|
|
|
|
# runtime build. |
|
1454
|
|
|
|
|
|
|
if ($use_prebuilt) { |
|
1455
|
|
|
|
|
|
|
local $@; |
|
1456
|
|
|
|
|
|
|
eval { |
|
1457
|
|
|
|
|
|
|
require XSLoader; |
|
1458
|
|
|
|
|
|
|
XSLoader::load( __PACKAGE__, $VERSION ); |
|
1459
|
|
|
|
|
|
|
$HAS_C = 1; |
|
1460
|
|
|
|
|
|
|
$C_SOURCE = 'prebuilt'; |
|
1461
|
|
|
|
|
|
|
}; |
|
1462
|
|
|
|
|
|
|
} |
|
1463
|
|
|
|
|
|
|
|
|
1464
|
|
|
|
|
|
|
# Classic runtime Inline::C build, MD5-cached under _Inline/. |
|
1465
|
|
|
|
|
|
|
# Reached when there is no matching prebuilt object: a source |
|
1466
|
|
|
|
|
|
|
# checkout, IF_RUNTIME_BUILD=1, or IF_* values differing from the |
|
1467
|
|
|
|
|
|
|
# ones recorded at configure time. Try compiling with OpenMP |
|
1468
|
|
|
|
|
|
|
# first; on any failure (compiler doesn't accept -fopenmp, libgomp |
|
1469
|
|
|
|
|
|
|
# missing, etc.) fall back to a serial build. |
|
1470
|
|
|
|
|
|
|
unless ( $HAS_C or $no_omp ) { |
|
1471
|
|
|
|
|
|
|
local $@; |
|
1472
|
|
|
|
|
|
|
eval { |
|
1473
|
|
|
|
|
|
|
require Inline; |
|
1474
|
|
|
|
|
|
|
Inline->import( |
|
1475
|
|
|
|
|
|
|
C => $omp_tag . $C_CODE, |
|
1476
|
|
|
|
|
|
|
CCFLAGS => '-fopenmp', |
|
1477
|
|
|
|
|
|
|
OPTIMIZE => $opt_level, |
|
1478
|
|
|
|
|
|
|
LIBS => '-lm -lgomp', |
|
1479
|
|
|
|
|
|
|
); |
|
1480
|
|
|
|
|
|
|
$HAS_C = 1; |
|
1481
|
|
|
|
|
|
|
$C_SOURCE = 'runtime'; |
|
1482
|
|
|
|
|
|
|
}; |
|
1483
|
|
|
|
|
|
|
} ## end unless ( $HAS_C or $no_omp ) |
|
1484
|
|
|
|
|
|
|
unless ($HAS_C) { |
|
1485
|
|
|
|
|
|
|
local $@; |
|
1486
|
|
|
|
|
|
|
eval { |
|
1487
|
|
|
|
|
|
|
require Inline; |
|
1488
|
|
|
|
|
|
|
Inline->import( |
|
1489
|
|
|
|
|
|
|
C => $serial_tag . $C_CODE, |
|
1490
|
|
|
|
|
|
|
OPTIMIZE => $opt_level, |
|
1491
|
|
|
|
|
|
|
LIBS => '-lm', |
|
1492
|
|
|
|
|
|
|
); |
|
1493
|
|
|
|
|
|
|
$HAS_C = 1; |
|
1494
|
|
|
|
|
|
|
$C_SOURCE = 'runtime'; |
|
1495
|
|
|
|
|
|
|
}; |
|
1496
|
|
|
|
|
|
|
} ## end unless ($HAS_C) |
|
1497
|
|
|
|
|
|
|
} ## end else [ if ( $ENV{IF_INSTALL_BUILD} ) ] |
|
1498
|
|
|
|
|
|
|
$OPT_LEVEL = $opt_level if $HAS_C; |
|
1499
|
|
|
|
|
|
|
|
|
1500
|
|
|
|
|
|
|
} ## end unless ( $ENV{IF_NO_C} ) |
|
1501
|
|
|
|
|
|
|
$HAS_OPENMP = ( $HAS_C && defined &has_openmp_xs && has_openmp_xs() ) ? 1 : 0; |
|
1502
|
|
|
|
|
|
|
$HAS_SIMD = ( $HAS_C && defined &has_simd_xs && has_simd_xs() ) ? 1 : 0; |
|
1503
|
|
|
|
|
|
|
} |
|
1504
|
|
|
|
|
|
|
|
|
1505
|
|
|
|
|
|
|
=encoding UTF-8 |
|
1506
|
|
|
|
|
|
|
|
|
1507
|
|
|
|
|
|
|
=head1 NAME |
|
1508
|
|
|
|
|
|
|
|
|
1509
|
|
|
|
|
|
|
Algorithm::Classifier::IsolationForest - unsupervised anomaly detection via Isolation Forest or Extended Isolation Forest |
|
1510
|
|
|
|
|
|
|
|
|
1511
|
|
|
|
|
|
|
=head1 SYNOPSIS |
|
1512
|
|
|
|
|
|
|
|
|
1513
|
|
|
|
|
|
|
use Algorithm::Classifier::IsolationForest; |
|
1514
|
|
|
|
|
|
|
|
|
1515
|
|
|
|
|
|
|
my @data = ([0.1, -0.2], [0.0, 0.1], [5.0, 6.0], ...); |
|
1516
|
|
|
|
|
|
|
|
|
1517
|
|
|
|
|
|
|
# Classic, axis-parallel Isolation Forest |
|
1518
|
|
|
|
|
|
|
my $iforest = Algorithm::Classifier::IsolationForest->new( |
|
1519
|
|
|
|
|
|
|
n_trees => 100, |
|
1520
|
|
|
|
|
|
|
sample_size => 256, |
|
1521
|
|
|
|
|
|
|
seed => 42, |
|
1522
|
|
|
|
|
|
|
); |
|
1523
|
|
|
|
|
|
|
$iforest->fit(\@data); |
|
1524
|
|
|
|
|
|
|
|
|
1525
|
|
|
|
|
|
|
my $scores = $iforest->score_samples(\@data); # arrayref, each in (0,1] |
|
1526
|
|
|
|
|
|
|
my $flags = $iforest->predict(\@data, 0.6); # arrayref of 0/1 |
|
1527
|
|
|
|
|
|
|
|
|
1528
|
|
|
|
|
|
|
# Save and reload |
|
1529
|
|
|
|
|
|
|
$iforest->save('model.json'); |
|
1530
|
|
|
|
|
|
|
my $reloaded = Algorithm::Classifier::IsolationForest->load('model.json'); |
|
1531
|
|
|
|
|
|
|
|
|
1532
|
|
|
|
|
|
|
# Extended Isolation Forest (oblique hyperplane splits) |
|
1533
|
|
|
|
|
|
|
my $eif = Algorithm::Classifier::IsolationForest->new( |
|
1534
|
|
|
|
|
|
|
mode => 'extended', |
|
1535
|
|
|
|
|
|
|
seed => 42, |
|
1536
|
|
|
|
|
|
|
); |
|
1537
|
|
|
|
|
|
|
$eif->fit(\@data); |
|
1538
|
|
|
|
|
|
|
|
|
1539
|
|
|
|
|
|
|
# Parallel training (fork-based, Unix-like platforms): build the |
|
1540
|
|
|
|
|
|
|
# n_trees across several worker processes. |
|
1541
|
|
|
|
|
|
|
my $iforest = Algorithm::Classifier::IsolationForest->new( |
|
1542
|
|
|
|
|
|
|
n_trees => 200, |
|
1543
|
|
|
|
|
|
|
sample_size => 256, |
|
1544
|
|
|
|
|
|
|
seed => 42, |
|
1545
|
|
|
|
|
|
|
parallel_fit => 4, # 4 forked workers |
|
1546
|
|
|
|
|
|
|
); |
|
1547
|
|
|
|
|
|
|
$iforest->fit(\@data); |
|
1548
|
|
|
|
|
|
|
|
|
1549
|
|
|
|
|
|
|
# Pre-pack a dataset to skip the per-call input-walk cost when the |
|
1550
|
|
|
|
|
|
|
# same data gets scored many times (interactive tuning, dashboards). |
|
1551
|
|
|
|
|
|
|
my $packed = $iforest->pack_data(\@data); |
|
1552
|
|
|
|
|
|
|
my $scores = $iforest->score_samples($packed); |
|
1553
|
|
|
|
|
|
|
my $flags = $iforest->predict($packed, 0.6); |
|
1554
|
|
|
|
|
|
|
|
|
1555
|
|
|
|
|
|
|
# Get scores and labels as two flat arrayrefs in one call -- cheaper |
|
1556
|
|
|
|
|
|
|
# than score_predict_samples when you don't need the paired shape. |
|
1557
|
|
|
|
|
|
|
my ($s, $l) = $iforest->score_predict_split(\@data, 0.6); |
|
1558
|
|
|
|
|
|
|
|
|
1559
|
|
|
|
|
|
|
=head1 DESCRIPTION |
|
1560
|
|
|
|
|
|
|
|
|
1561
|
|
|
|
|
|
|
Isolation Forest (Liu, Fei Tony & Ting, Kai & Zhou, Zhi-Hua, 2008) detects anomalies by random |
|
1562
|
|
|
|
|
|
|
partitioning rather than by modelling normal points. Each tree repeatedly |
|
1563
|
|
|
|
|
|
|
splits the data. Points that get isolated after only a few splits are likely |
|
1564
|
|
|
|
|
|
|
anomalies. The score is the average isolation depth across many trees, |
|
1565
|
|
|
|
|
|
|
normalised so values approach 1 for anomalies and stay below 0.5 for normal |
|
1566
|
|
|
|
|
|
|
points. |
|
1567
|
|
|
|
|
|
|
|
|
1568
|
|
|
|
|
|
|
In extended mode the module implements the Extended Isolation Forest |
|
1569
|
|
|
|
|
|
|
variant. Each split is a random hyperplane instead of an axis-aligned cut, |
|
1570
|
|
|
|
|
|
|
which removes the rectangular, axis-aligned bias in the score field and |
|
1571
|
|
|
|
|
|
|
tends to help on elongated or multi-modal data. |
|
1572
|
|
|
|
|
|
|
|
|
1573
|
|
|
|
|
|
|
psi referenced below is ψ or the pitchfork math symbol referenced in the paper, |
|
1574
|
|
|
|
|
|
|
Liu, Fei Tony & Ting, Kai & Zhou, Zhi-Hua. (2008). Isolation Forest. 413 - 422. 10.1109/ICDM.2008.17. |
|
1575
|
|
|
|
|
|
|
|
|
1576
|
|
|
|
|
|
|
... or max samples. |
|
1577
|
|
|
|
|
|
|
|
|
1578
|
|
|
|
|
|
|
L |
|
1579
|
|
|
|
|
|
|
|
|
1580
|
|
|
|
|
|
|
=head1 NATIVE ACCELERATION (Inline::C and OpenMP) |
|
1581
|
|
|
|
|
|
|
|
|
1582
|
|
|
|
|
|
|
Both the scoring hot path (C, C, C, |
|
1583
|
|
|
|
|
|
|
C, and C) and the C |
|
1584
|
|
|
|
|
|
|
tree builder are automatically accelerated through |
|
1585
|
|
|
|
|
|
|
L when it is installed and a working C compiler is reachable. |
|
1586
|
|
|
|
|
|
|
If the toolchain also accepts C<-fopenmp> and can link against |
|
1587
|
|
|
|
|
|
|
C, the per-point tree walk runs in parallel across all |
|
1588
|
|
|
|
|
|
|
available CPU cores using OpenMP, and the extended-mode oblique dot |
|
1589
|
|
|
|
|
|
|
product is vectorised via C<#pragma omp simd> -- which on modern x86 |
|
1590
|
|
|
|
|
|
|
compilers translates to an unrolled FMA / AVX gather chain that's |
|
1591
|
|
|
|
|
|
|
substantially faster for high-feature-count extended models. |
|
1592
|
|
|
|
|
|
|
|
|
1593
|
|
|
|
|
|
|
C's tree builder (subsampling plus the recursive axis/oblique |
|
1594
|
|
|
|
|
|
|
split search) runs in C the same way when C is on, replacing the |
|
1595
|
|
|
|
|
|
|
per-node Perl arrayref copying with plain int-array partitioning -- |
|
1596
|
|
|
|
|
|
|
typically an order of magnitude faster, and dramatically more so at |
|
1597
|
|
|
|
|
|
|
higher feature counts where the pure-Perl per-cell loop dominates. Its |
|
1598
|
|
|
|
|
|
|
random draws go through the same generator C/C use |
|
1599
|
|
|
|
|
|
|
internally, in the same call order the pure-Perl builder uses, so a |
|
1600
|
|
|
|
|
|
|
given C produces bit-identical trees whether C is on or |
|
1601
|
|
|
|
|
|
|
off -- switching backends changes only how fast the model is built, not |
|
1602
|
|
|
|
|
|
|
the model itself. |
|
1603
|
|
|
|
|
|
|
|
|
1604
|
|
|
|
|
|
|
By default this C builder is single-threaded per call, because Perl's |
|
1605
|
|
|
|
|
|
|
RNG state isn't safe to share across OpenMP threads. Two ways to scale |
|
1606
|
|
|
|
|
|
|
fit() across cores are available (see below for why they don't compose): |
|
1607
|
|
|
|
|
|
|
|
|
1608
|
|
|
|
|
|
|
=over 4 |
|
1609
|
|
|
|
|
|
|
|
|
1610
|
|
|
|
|
|
|
=item * C forks N worker processes, each building its |
|
1611
|
|
|
|
|
|
|
share of the trees with the (still single-threaded) C builder. Fixed |
|
1612
|
|
|
|
|
|
|
IPC/serialisation overhead per worker means this can cost more than it |
|
1613
|
|
|
|
|
|
|
saves once a fit already completes in milliseconds; it's most useful |
|
1614
|
|
|
|
|
|
|
once a single-process fit is large enough that the fork/Storable |
|
1615
|
|
|
|
|
|
|
overhead is small relative to the work being split. |
|
1616
|
|
|
|
|
|
|
|
|
1617
|
|
|
|
|
|
|
=item * C builds trees across OpenMP threads within a |
|
1618
|
|
|
|
|
|
|
single process (one tree per thread), using a separate, thread-safe |
|
1619
|
|
|
|
|
|
|
PRNG seeded per tree index instead of Perl's C. This means |
|
1620
|
|
|
|
|
|
|
trees built with C are I bit-identical to the |
|
1621
|
|
|
|
|
|
|
default C path for the same seed -- but a fixed seed and |
|
1622
|
|
|
|
|
|
|
C still reproduce the same trees regardless of |
|
1623
|
|
|
|
|
|
|
C or how OpenMP schedules the work. It's off by |
|
1624
|
|
|
|
|
|
|
default (unlike C/C, which only ever change speed, |
|
1625
|
|
|
|
|
|
|
this changes which trees get built) and only takes effect when C |
|
1626
|
|
|
|
|
|
|
is also on and OpenMP is linked in. |
|
1627
|
|
|
|
|
|
|
|
|
1628
|
|
|
|
|
|
|
=back |
|
1629
|
|
|
|
|
|
|
|
|
1630
|
|
|
|
|
|
|
These two do NOT compose, despite both existing to parallelise fit(). |
|
1631
|
|
|
|
|
|
|
A process that has run any OpenMP region -- including plain |
|
1632
|
|
|
|
|
|
|
C/C with the default C -- and |
|
1633
|
|
|
|
|
|
|
then Cs (as C does) hands each child a copy of |
|
1634
|
|
|
|
|
|
|
libgomp's thread pool whose worker threads did not survive the fork. A |
|
1635
|
|
|
|
|
|
|
child that then starts its own C<#pragma omp parallel> region (as |
|
1636
|
|
|
|
|
|
|
C would) tries to reuse that now-invalid pool and |
|
1637
|
|
|
|
|
|
|
hangs. This is a general limitation of combining C with OpenMP, |
|
1638
|
|
|
|
|
|
|
not something fixable from Perl, so C's forked workers |
|
1639
|
|
|
|
|
|
|
always use the single-threaded C builder regardless of |
|
1640
|
|
|
|
|
|
|
C -- setting both just means C wins and |
|
1641
|
|
|
|
|
|
|
C has no effect for that call. |
|
1642
|
|
|
|
|
|
|
|
|
1643
|
|
|
|
|
|
|
Detection happens once when the module is loaded. When the |
|
1644
|
|
|
|
|
|
|
distribution was installed with C available, the C backend |
|
1645
|
|
|
|
|
|
|
was already compiled during C and the installed object is loaded |
|
1646
|
|
|
|
|
|
|
directly (see L below); |
|
1647
|
|
|
|
|
|
|
otherwise the backend is compiled on first load and the artefact is |
|
1648
|
|
|
|
|
|
|
cached under C<_Inline/> and reused on subsequent runs. Five package |
|
1649
|
|
|
|
|
|
|
variables report what the load picked up: |
|
1650
|
|
|
|
|
|
|
|
|
1651
|
|
|
|
|
|
|
$Algorithm::Classifier::IsolationForest::HAS_C # 0/1 |
|
1652
|
|
|
|
|
|
|
$Algorithm::Classifier::IsolationForest::HAS_OPENMP # 0/1 |
|
1653
|
|
|
|
|
|
|
$Algorithm::Classifier::IsolationForest::HAS_SIMD # 0/1 (OpenMP 4.0+) |
|
1654
|
|
|
|
|
|
|
$Algorithm::Classifier::IsolationForest::OPT_LEVEL # e.g. "-O3 -march=native", '' if HAS_C is 0 |
|
1655
|
|
|
|
|
|
|
$Algorithm::Classifier::IsolationForest::C_SOURCE # 'prebuilt' / 'runtime', '' if HAS_C is 0 |
|
1656
|
|
|
|
|
|
|
|
|
1657
|
|
|
|
|
|
|
Neither dependency is required. Without C the module falls |
|
1658
|
|
|
|
|
|
|
back to a pure-Perl implementation that produces identical results, just |
|
1659
|
|
|
|
|
|
|
slower; without OpenMP the C backend runs single-threaded. |
|
1660
|
|
|
|
|
|
|
|
|
1661
|
|
|
|
|
|
|
The bundled C subcommand performs a tiny fit + score and |
|
1662
|
|
|
|
|
|
|
prints which backend is active (including the build flags below), which |
|
1663
|
|
|
|
|
|
|
is the recommended way to verify the build picked up the optional |
|
1664
|
|
|
|
|
|
|
dependencies on a given machine. |
|
1665
|
|
|
|
|
|
|
|
|
1666
|
|
|
|
|
|
|
=head2 Compile at install time (the prebuilt object) |
|
1667
|
|
|
|
|
|
|
|
|
1668
|
|
|
|
|
|
|
When C is usable while the distribution itself is being |
|
1669
|
|
|
|
|
|
|
built, C arranges for the C backend to be compiled |
|
1670
|
|
|
|
|
|
|
once during C and installed alongside the module like any XS |
|
1671
|
|
|
|
|
|
|
object. At run time that object is loaded directly through |
|
1672
|
|
|
|
|
|
|
L: no C compiler, no C modules, and no C<_Inline/> |
|
1673
|
|
|
|
|
|
|
cache directory are needed on the machine the module ends up running |
|
1674
|
|
|
|
|
|
|
on, and the first-load compile pause disappears entirely. |
|
1675
|
|
|
|
|
|
|
|
|
1676
|
|
|
|
|
|
|
On x86-64 hardware from roughly the last decade, |
|
1677
|
|
|
|
|
|
|
C is a reasonable configure line: |
|
1678
|
|
|
|
|
|
|
it bakes AVX2 + FMA (without AVX-512) into the prebuilt object, which |
|
1679
|
|
|
|
|
|
|
can speed up extended-mode scoring (how much is hardware-dependent -- |
|
1680
|
|
|
|
|
|
|
benchmark with C before assuming) while avoiding the |
|
1681
|
|
|
|
|
|
|
C<-march=native> caveats described under L. |
|
1682
|
|
|
|
|
|
|
Bit-for-bit result parity with the pure-Perl backend is preserved |
|
1683
|
|
|
|
|
|
|
either way (see C below). |
|
1684
|
|
|
|
|
|
|
|
|
1685
|
|
|
|
|
|
|
The C build flags described below are captured when |
|
1686
|
|
|
|
|
|
|
C runs -- set them in the environment of I |
|
1687
|
|
|
|
|
|
|
command, not of C -- and recorded in the generated |
|
1688
|
|
|
|
|
|
|
C module, which |
|
1689
|
|
|
|
|
|
|
thereby also fixes what the prebuilt object was compiled with. At run |
|
1690
|
|
|
|
|
|
|
time the recorded values serve as the defaults, so a process started |
|
1691
|
|
|
|
|
|
|
with no C variables set uses the prebuilt object as-is. |
|
1692
|
|
|
|
|
|
|
|
|
1693
|
|
|
|
|
|
|
Setting C variables at run time keeps working exactly as in |
|
1694
|
|
|
|
|
|
|
releases without prebuilt support: if the requested flags differ from |
|
1695
|
|
|
|
|
|
|
the recorded ones, the prebuilt object (compiled with the wrong flags |
|
1696
|
|
|
|
|
|
|
for the request) is skipped and the module compiles at first load into |
|
1697
|
|
|
|
|
|
|
C<_Inline/> -- which does need C and a compiler on that |
|
1698
|
|
|
|
|
|
|
machine. Two related knobs exist: |
|
1699
|
|
|
|
|
|
|
|
|
1700
|
|
|
|
|
|
|
=over 4 |
|
1701
|
|
|
|
|
|
|
|
|
1702
|
|
|
|
|
|
|
=item * C -- ignore the prebuilt object |
|
1703
|
|
|
|
|
|
|
unconditionally and compile at first load even though the requested |
|
1704
|
|
|
|
|
|
|
flags match the recorded ones. Useful when the installed object is |
|
1705
|
|
|
|
|
|
|
suspect (built on a different CPU than it now runs on, linked against a |
|
1706
|
|
|
|
|
|
|
libgomp that has since changed) or to A/B a fresh local build against |
|
1707
|
|
|
|
|
|
|
the shipped one. |
|
1708
|
|
|
|
|
|
|
|
|
1709
|
|
|
|
|
|
|
=item * C -- internal; set by the generated |
|
1710
|
|
|
|
|
|
|
Makefile rule that performs the install-time compile. Not meant for |
|
1711
|
|
|
|
|
|
|
manual use. |
|
1712
|
|
|
|
|
|
|
|
|
1713
|
|
|
|
|
|
|
=back |
|
1714
|
|
|
|
|
|
|
|
|
1715
|
|
|
|
|
|
|
If the prebuilt object cannot be loaded for any reason (deleted, built |
|
1716
|
|
|
|
|
|
|
against a different perl, version mismatch after an upgrade), the |
|
1717
|
|
|
|
|
|
|
module quietly falls through the same chain as always: runtime |
|
1718
|
|
|
|
|
|
|
Inline::C build first, pure Perl last. |
|
1719
|
|
|
|
|
|
|
|
|
1720
|
|
|
|
|
|
|
=head2 Tuning the C build |
|
1721
|
|
|
|
|
|
|
|
|
1722
|
|
|
|
|
|
|
These environment variables are read once, the first time the module is |
|
1723
|
|
|
|
|
|
|
loaded, so they must be set before that -- e.g. in the shell before |
|
1724
|
|
|
|
|
|
|
running a script, not via C<%ENV> inside the script itself. They are |
|
1725
|
|
|
|
|
|
|
also read by C to pick the flags baked into the |
|
1726
|
|
|
|
|
|
|
prebuilt object (see above); at run time they override the recorded |
|
1727
|
|
|
|
|
|
|
configure-time values, at the price of a runtime compile. |
|
1728
|
|
|
|
|
|
|
|
|
1729
|
|
|
|
|
|
|
=over 4 |
|
1730
|
|
|
|
|
|
|
|
|
1731
|
|
|
|
|
|
|
=item * C -- skip attempting to build the C backend entirely. |
|
1732
|
|
|
|
|
|
|
Equivalent to constructing every instance with C 0>, but |
|
1733
|
|
|
|
|
|
|
without needing to touch every call site; useful for a clean pure-Perl |
|
1734
|
|
|
|
|
|
|
timing baseline, or to avoid the compile attempt's overhead/noise on a |
|
1735
|
|
|
|
|
|
|
host known to lack a C compiler (the attempt already fails gracefully |
|
1736
|
|
|
|
|
|
|
without this, so it's a convenience, not a correctness fix). |
|
1737
|
|
|
|
|
|
|
|
|
1738
|
|
|
|
|
|
|
=item * C (or C<-O0>/C<-O1>/C<-Os>/C<-Og>/C<-Oz>) -- override |
|
1739
|
|
|
|
|
|
|
the default C<-O3>, e.g. to shorten build time while iterating, or work |
|
1740
|
|
|
|
|
|
|
around a miscompile on an unusual toolchain. Invalid values are ignored |
|
1741
|
|
|
|
|
|
|
with a warning rather than passed through, since this string reaches a |
|
1742
|
|
|
|
|
|
|
compiler command line. |
|
1743
|
|
|
|
|
|
|
|
|
1744
|
|
|
|
|
|
|
=item * CvalueE> -- adds C<-march=EvalueE> so the |
|
1745
|
|
|
|
|
|
|
compiler can target specific instruction-set extensions (AVX2 gather + |
|
1746
|
|
|
|
|
|
|
FMA, etc.) for the extended-mode oblique dot product and the fit-time |
|
1747
|
|
|
|
|
|
|
min/max scan's C<#pragma omp simd> loops. Accepts values like |
|
1748
|
|
|
|
|
|
|
C, C, or C -- whatever your compiler's |
|
1749
|
|
|
|
|
|
|
C<-march=> accepts. Also validated (a restricted character set, not |
|
1750
|
|
|
|
|
|
|
passed through as-is) for the same reason as C. The special |
|
1751
|
|
|
|
|
|
|
value C (or an empty string) opts out of any arch recorded at |
|
1752
|
|
|
|
|
|
|
configure time, yielding a plain build. Whenever a C<-march> is in |
|
1753
|
|
|
|
|
|
|
effect the build also adds C<-ffp-contract=off>: with FMA available |
|
1754
|
|
|
|
|
|
|
the compiler would otherwise contract C into fused |
|
1755
|
|
|
|
|
|
|
multiply-adds whose different rounding breaks the guarantee that |
|
1756
|
|
|
|
|
|
|
C 1> and C 0> build bit-identical trees (the |
|
1757
|
|
|
|
|
|
|
C<-march> speedup comes from vectorization, not contraction, so this |
|
1758
|
|
|
|
|
|
|
costs essentially nothing). |
|
1759
|
|
|
|
|
|
|
|
|
1760
|
|
|
|
|
|
|
=item * C -- shorthand for C; ignored if |
|
1761
|
|
|
|
|
|
|
C is also set. Prefer a specific C value over this on |
|
1762
|
|
|
|
|
|
|
a machine you don't control exclusively (a shared build host, a |
|
1763
|
|
|
|
|
|
|
container base image): blanket C<-march=native> pulls in whatever |
|
1764
|
|
|
|
|
|
|
instruction sets the build host happens to have, including AVX-512 on |
|
1765
|
|
|
|
|
|
|
some Intel CPUs -- which is known to trigger clock throttling under |
|
1766
|
|
|
|
|
|
|
sustained heavy use and can make throughput I than a |
|
1767
|
|
|
|
|
|
|
conservative target like C (AVX2, no AVX-512). If in doubt, |
|
1768
|
|
|
|
|
|
|
benchmark both before committing to one. |
|
1769
|
|
|
|
|
|
|
|
|
1770
|
|
|
|
|
|
|
=item * C -- build (or select) the serial C backend: the |
|
1771
|
|
|
|
|
|
|
OpenMP compile attempt is skipped entirely, so the resulting object has |
|
1772
|
|
|
|
|
|
|
no libgomp linkage and never starts an OpenMP runtime inside the |
|
1773
|
|
|
|
|
|
|
process. This differs from C, which merely runs the |
|
1774
|
|
|
|
|
|
|
parallel code on one thread but still loads libgomp. Set at |
|
1775
|
|
|
|
|
|
|
C time it yields a serial prebuilt object; set at run |
|
1776
|
|
|
|
|
|
|
time against an OpenMP prebuilt install it triggers a runtime serial |
|
1777
|
|
|
|
|
|
|
build (needing a compiler). An explicit C re-enables |
|
1778
|
|
|
|
|
|
|
OpenMP over a serial configure-time default. |
|
1779
|
|
|
|
|
|
|
|
|
1780
|
|
|
|
|
|
|
=back |
|
1781
|
|
|
|
|
|
|
|
|
1782
|
|
|
|
|
|
|
Whichever of these are used, the cached artefact under C<_Inline/> is |
|
1783
|
|
|
|
|
|
|
pinned to that build's instruction set -- delete C<_Inline/> (or use a |
|
1784
|
|
|
|
|
|
|
separate one per host) if the directory is shared across machines with |
|
1785
|
|
|
|
|
|
|
different CPUs, or a stale binary built for a narrower instruction set |
|
1786
|
|
|
|
|
|
|
than the current host will simply keep being reused. |
|
1787
|
|
|
|
|
|
|
|
|
1788
|
|
|
|
|
|
|
=head2 Tuning the OpenMP runtime |
|
1789
|
|
|
|
|
|
|
|
|
1790
|
|
|
|
|
|
|
These are standard OpenMP environment variables libgomp already reads |
|
1791
|
|
|
|
|
|
|
at run time (set before running your script, no module-specific |
|
1792
|
|
|
|
|
|
|
handling needed) -- listed here because they matter most for exactly |
|
1793
|
|
|
|
|
|
|
the workloads this module has: C's per-point parallel |
|
1794
|
|
|
|
|
|
|
loop and C's per-tree parallel loop. |
|
1795
|
|
|
|
|
|
|
|
|
1796
|
|
|
|
|
|
|
=over 4 |
|
1797
|
|
|
|
|
|
|
|
|
1798
|
|
|
|
|
|
|
=item * C -- caps how many threads a parallel region |
|
1799
|
|
|
|
|
|
|
uses. Useful to leave headroom for other work sharing the machine, or |
|
1800
|
|
|
|
|
|
|
to pin down C reproducibility checks (see its docs |
|
1801
|
|
|
|
|
|
|
above: results don't depend on this, but it's a natural thing to vary |
|
1802
|
|
|
|
|
|
|
when confirming that). |
|
1803
|
|
|
|
|
|
|
|
|
1804
|
|
|
|
|
|
|
=item * C / C -- on multi-socket |
|
1805
|
|
|
|
|
|
|
or otherwise NUMA machines, pins each thread to a core near where its |
|
1806
|
|
|
|
|
|
|
data already lives instead of letting the OS scheduler migrate threads |
|
1807
|
|
|
|
|
|
|
across sockets mid-run. Both C (each thread scans its own |
|
1808
|
|
|
|
|
|
|
slice of the packed query buffer) and C (each thread |
|
1809
|
|
|
|
|
|
|
builds one tree from packed training data) benefit from this when the |
|
1810
|
|
|
|
|
|
|
input is large enough to not fit comfortably in one socket's cache. |
|
1811
|
|
|
|
|
|
|
|
|
1812
|
|
|
|
|
|
|
=back |
|
1813
|
|
|
|
|
|
|
|
|
1814
|
|
|
|
|
|
|
These cost nothing to try -- unlike C/C, they're |
|
1815
|
|
|
|
|
|
|
read fresh every run, not baked into a cached binary, so there's no |
|
1816
|
|
|
|
|
|
|
downside to experimenting per invocation. |
|
1817
|
|
|
|
|
|
|
|
|
1818
|
|
|
|
|
|
|
=head1 GENERAL METHODS |
|
1819
|
|
|
|
|
|
|
|
|
1820
|
|
|
|
|
|
|
=head2 new(%args) |
|
1821
|
|
|
|
|
|
|
|
|
1822
|
|
|
|
|
|
|
Inits the object. |
|
1823
|
|
|
|
|
|
|
|
|
1824
|
|
|
|
|
|
|
- n_trees :: number of isolation trees in the ensemble |
|
1825
|
|
|
|
|
|
|
default :: 100 |
|
1826
|
|
|
|
|
|
|
|
|
1827
|
|
|
|
|
|
|
- sample_size :: sub-sample size used to build each tree... max samples |
|
1828
|
|
|
|
|
|
|
default :: 256 |
|
1829
|
|
|
|
|
|
|
|
|
1830
|
|
|
|
|
|
|
- max_depth :: per-tree height limit... if not defined is set to ceil(log2(psi)) |
|
1831
|
|
|
|
|
|
|
default :: undef |
|
1832
|
|
|
|
|
|
|
|
|
1833
|
|
|
|
|
|
|
- seed :: optional integer to seed srand with for reproducible trees... |
|
1834
|
|
|
|
|
|
|
see perldoc -f srand for more info. This number is processed via abs(int()). |
|
1835
|
|
|
|
|
|
|
default :: undef |
|
1836
|
|
|
|
|
|
|
|
|
1837
|
|
|
|
|
|
|
- mode :: if it should be IF or EIF |
|
1838
|
|
|
|
|
|
|
axis :: classic axis-parallel splits (IF) |
|
1839
|
|
|
|
|
|
|
extended :: oblique hyperplane splits (EIF) |
|
1840
|
|
|
|
|
|
|
default :: axis |
|
1841
|
|
|
|
|
|
|
|
|
1842
|
|
|
|
|
|
|
- extension_level :: extended mode only... how many features take partin each |
|
1843
|
|
|
|
|
|
|
split. 0 behaves like a single-feature (axis) cut; the |
|
1844
|
|
|
|
|
|
|
maximum (n_features - 1) uses every varying feature. undef |
|
1845
|
|
|
|
|
|
|
=> maximum. Clamped to [0, n_features - 1] at fit time. |
|
1846
|
|
|
|
|
|
|
|
|
1847
|
|
|
|
|
|
|
- contamination :: expected fraction of anomalies, in (0, 0.5]. When given, |
|
1848
|
|
|
|
|
|
|
fit() learns a score threshold that flags this fraction of |
|
1849
|
|
|
|
|
|
|
the training set, and predict() uses it by default. undef |
|
1850
|
|
|
|
|
|
|
=> no learned threshold (predict() falls back to 0.5). |
|
1851
|
|
|
|
|
|
|
default :: undef |
|
1852
|
|
|
|
|
|
|
|
|
1853
|
|
|
|
|
|
|
- missing :: how fit() treats undef (missing) feature cells. Scoring always |
|
1854
|
|
|
|
|
|
|
tolerates undef regardless of this setting; it governs fit(). |
|
1855
|
|
|
|
|
|
|
die :: croak from fit() if the training data contains any |
|
1856
|
|
|
|
|
|
|
undef cell. Scoring still maps undef to 0 (the |
|
1857
|
|
|
|
|
|
|
long-standing behaviour), so a model fitted on clean |
|
1858
|
|
|
|
|
|
|
data can still score rows with missing features. |
|
1859
|
|
|
|
|
|
|
zero :: treat a missing cell as the value 0, at fit and score. |
|
1860
|
|
|
|
|
|
|
impute :: replace a missing cell with the per-feature mean (or |
|
1861
|
|
|
|
|
|
|
median, see impute_with) learned from the present |
|
1862
|
|
|
|
|
|
|
values at fit time. The fill vector is stored on the |
|
1863
|
|
|
|
|
|
|
model and reused for scoring and persistence. |
|
1864
|
|
|
|
|
|
|
nan :: build feature ranges from present values only and route |
|
1865
|
|
|
|
|
|
|
a point missing the split feature to the right child, |
|
1866
|
|
|
|
|
|
|
consistently at fit and score time. Missingness is |
|
1867
|
|
|
|
|
|
|
preserved as signal rather than filled. |
|
1868
|
|
|
|
|
|
|
default :: die |
|
1869
|
|
|
|
|
|
|
|
|
1870
|
|
|
|
|
|
|
- impute_with :: 'mean' or 'median'; the statistic used to compute the |
|
1871
|
|
|
|
|
|
|
per-feature fill under missing => 'impute'. Ignored otherwise. |
|
1872
|
|
|
|
|
|
|
default :: mean |
|
1873
|
|
|
|
|
|
|
|
|
1874
|
|
|
|
|
|
|
- parallel_fit :: positive integer N => build the trees across N forked |
|
1875
|
|
|
|
|
|
|
worker processes during fit(). Each worker gets a derived seed |
|
1876
|
|
|
|
|
|
|
(parent seed + worker_id * 1009) so the parallel fit is |
|
1877
|
|
|
|
|
|
|
reproducible across runs at fixed worker count -- but the trees |
|
1878
|
|
|
|
|
|
|
produced are NOT bit-identical to a serial fit with the same |
|
1879
|
|
|
|
|
|
|
seed, because the RNG draws happen in a different order. |
|
1880
|
|
|
|
|
|
|
Inference is unaffected. Falls back silently to serial on |
|
1881
|
|
|
|
|
|
|
platforms without a real fork() (e.g. Windows without Cygwin). |
|
1882
|
|
|
|
|
|
|
default :: undef (serial) |
|
1883
|
|
|
|
|
|
|
|
|
1884
|
|
|
|
|
|
|
- use_c :: boolean, override whether the Inline::C backend is used for |
|
1885
|
|
|
|
|
|
|
both scoring and fit()'s tree builder. When false the instance |
|
1886
|
|
|
|
|
|
|
falls back to pure Perl for both even if the C backend compiled |
|
1887
|
|
|
|
|
|
|
successfully. When true (or unset) the C backend is used if |
|
1888
|
|
|
|
|
|
|
available ($HAS_C). fit() with use_c on produces bit-identical |
|
1889
|
|
|
|
|
|
|
trees to use_c off for the same seed -- only build speed differs. |
|
1890
|
|
|
|
|
|
|
default :: $HAS_C |
|
1891
|
|
|
|
|
|
|
|
|
1892
|
|
|
|
|
|
|
- use_openmp :: boolean, override whether OpenMP parallel scoring is |
|
1893
|
|
|
|
|
|
|
used inside score_all_xs(). When false the C tree walk runs |
|
1894
|
|
|
|
|
|
|
single-threaded even if OpenMP was linked in. Ignored when |
|
1895
|
|
|
|
|
|
|
use_c is false (pure Perl has no OpenMP path). |
|
1896
|
|
|
|
|
|
|
default :: $HAS_OPENMP |
|
1897
|
|
|
|
|
|
|
|
|
1898
|
|
|
|
|
|
|
- use_openmp_fit :: boolean, build fit()'s trees across OpenMP threads |
|
1899
|
|
|
|
|
|
|
(one tree per thread) instead of the single-threaded C builder. |
|
1900
|
|
|
|
|
|
|
Opt-in and off by default: unlike use_c/use_openmp, this changes |
|
1901
|
|
|
|
|
|
|
which trees get built. Perl's RNG isn't safe to call from |
|
1902
|
|
|
|
|
|
|
multiple OS threads sharing one interpreter, so this path seeds |
|
1903
|
|
|
|
|
|
|
an independent PRNG per tree from the tree index rather than |
|
1904
|
|
|
|
|
|
|
Drand01() -- trees differ from the use_c (single-threaded) |
|
1905
|
|
|
|
|
|
|
and pure-Perl paths even with the same seed, though a fixed |
|
1906
|
|
|
|
|
|
|
seed and n_trees still reproduce the same trees regardless of |
|
1907
|
|
|
|
|
|
|
OMP_NUM_THREADS or scheduling. Does NOT compose with |
|
1908
|
|
|
|
|
|
|
parallel_fit: a forked child starting its own OpenMP region |
|
1909
|
|
|
|
|
|
|
after the parent process has used OpenMP for anything can |
|
1910
|
|
|
|
|
|
|
hang (a general fork()+libgomp limitation), so parallel_fit's |
|
1911
|
|
|
|
|
|
|
workers always use the single-threaded C builder regardless |
|
1912
|
|
|
|
|
|
|
of this setting -- setting both just means parallel_fit wins. |
|
1913
|
|
|
|
|
|
|
Ignored (clamped to 0) when use_c is false or OpenMP isn't |
|
1914
|
|
|
|
|
|
|
linked in. |
|
1915
|
|
|
|
|
|
|
default :: 0 |
|
1916
|
|
|
|
|
|
|
|
|
1917
|
|
|
|
|
|
|
Note: log2 under Perl is as below... |
|
1918
|
|
|
|
|
|
|
|
|
1919
|
|
|
|
|
|
|
log($psi) / log(2) |
|
1920
|
|
|
|
|
|
|
|
|
1921
|
|
|
|
|
|
|
=cut |
|
1922
|
|
|
|
|
|
|
|
|
1923
|
|
|
|
|
|
|
sub new { |
|
1924
|
164
|
|
|
164
|
1
|
6042501
|
my ( $class, %args ) = @_; |
|
1925
|
|
|
|
|
|
|
|
|
1926
|
164
|
|
100
|
|
|
1010
|
my $mode = $args{mode} // 'axis'; |
|
1927
|
164
|
100
|
100
|
|
|
904
|
croak "mode must be 'axis' or 'extended'" |
|
1928
|
|
|
|
|
|
|
unless $mode eq 'axis' || $mode eq 'extended'; |
|
1929
|
|
|
|
|
|
|
|
|
1930
|
|
|
|
|
|
|
# How fit() treats undef (missing) feature cells. Scoring always |
|
1931
|
|
|
|
|
|
|
# tolerates undef regardless of this setting -- it governs fit only. |
|
1932
|
|
|
|
|
|
|
# die :: croak if the training data contains any undef cell (default) |
|
1933
|
|
|
|
|
|
|
# zero :: treat a missing cell as the value 0 |
|
1934
|
|
|
|
|
|
|
# impute :: replace a missing cell with the per-feature mean/median |
|
1935
|
|
|
|
|
|
|
# learned from the present values at fit time |
|
1936
|
|
|
|
|
|
|
# nan :: build ranges over present values only and route a point |
|
1937
|
|
|
|
|
|
|
# missing the split feature consistently to one branch, at |
|
1938
|
|
|
|
|
|
|
# both fit and score time |
|
1939
|
163
|
|
100
|
|
|
653
|
my $missing = $args{missing} // 'die'; |
|
1940
|
163
|
100
|
|
|
|
1373
|
croak "missing must be one of: die, zero, impute, nan" |
|
1941
|
|
|
|
|
|
|
unless $missing =~ /\A(?:die|zero|impute|nan)\z/; |
|
1942
|
|
|
|
|
|
|
|
|
1943
|
162
|
|
100
|
|
|
570
|
my $impute_with = $args{impute_with} // 'mean'; |
|
1944
|
162
|
100
|
|
|
|
804
|
croak "impute_with must be 'mean' or 'median'" |
|
1945
|
|
|
|
|
|
|
unless $impute_with =~ /\A(?:mean|median)\z/; |
|
1946
|
|
|
|
|
|
|
|
|
1947
|
161
|
100
|
|
|
|
457
|
if ( defined( $args{seed} ) ) { |
|
1948
|
131
|
|
|
|
|
352
|
$args{seed} = abs( int( $args{seed} ) ); |
|
1949
|
|
|
|
|
|
|
} |
|
1950
|
|
|
|
|
|
|
|
|
1951
|
|
|
|
|
|
|
# Clamp the accel knobs against what the build actually has. Passing |
|
1952
|
|
|
|
|
|
|
# use_c => 1 on a machine where Inline::C never compiled would otherwise |
|
1953
|
|
|
|
|
|
|
# leave score_samples() calling an undefined XS sub at first use. |
|
1954
|
|
|
|
|
|
|
# OpenMP is meaningless without the C tree walk, so force it off |
|
1955
|
|
|
|
|
|
|
# whenever the C backend is off -- matches the documented |
|
1956
|
|
|
|
|
|
|
# "Ignored when use_c is false" semantics. |
|
1957
|
|
|
|
|
|
|
my $use_c |
|
1958
|
|
|
|
|
|
|
= defined $args{use_c} |
|
1959
|
161
|
100
|
100
|
|
|
762
|
? ( $args{use_c} && $HAS_C ? 1 : 0 ) |
|
|
|
100
|
|
|
|
|
|
|
1960
|
|
|
|
|
|
|
: $HAS_C; |
|
1961
|
|
|
|
|
|
|
my $use_openmp |
|
1962
|
|
|
|
|
|
|
= defined $args{use_openmp} |
|
1963
|
161
|
100
|
100
|
|
|
390
|
? ( $args{use_openmp} && $HAS_OPENMP ? 1 : 0 ) |
|
|
|
100
|
|
|
|
|
|
|
1964
|
|
|
|
|
|
|
: $HAS_OPENMP; |
|
1965
|
161
|
100
|
|
|
|
347
|
$use_openmp = 0 unless $use_c; |
|
1966
|
|
|
|
|
|
|
|
|
1967
|
|
|
|
|
|
|
# Opt-in only (default 0, not $HAS_OPENMP): this path changes which |
|
1968
|
|
|
|
|
|
|
# trees fit() builds (see docs above), unlike use_c/use_openmp which |
|
1969
|
|
|
|
|
|
|
# only change speed. Clamped the same way use_openmp is. |
|
1970
|
161
|
100
|
33
|
|
|
592
|
my $use_openmp_fit = ( $args{use_openmp_fit} && $HAS_OPENMP && $use_c ) ? 1 : 0; |
|
1971
|
|
|
|
|
|
|
|
|
1972
|
|
|
|
|
|
|
my $self = { |
|
1973
|
|
|
|
|
|
|
n_trees => $args{n_trees} // 100, |
|
1974
|
|
|
|
|
|
|
sample_size => $args{sample_size} // 256, |
|
1975
|
|
|
|
|
|
|
max_depth => $args{max_depth}, # undef => auto |
|
1976
|
|
|
|
|
|
|
seed => $args{seed}, # undef => non-deterministic |
|
1977
|
|
|
|
|
|
|
mode => $mode, |
|
1978
|
|
|
|
|
|
|
extension_level => $args{extension_level}, # undef => max, resolved in fit() |
|
1979
|
|
|
|
|
|
|
contamination => $args{contamination}, # undef => no learned threshold |
|
1980
|
|
|
|
|
|
|
parallel_fit => $args{parallel_fit}, # undef/0/1 => serial; N>1 => fork |
|
1981
|
|
|
|
|
|
|
missing => $missing, # die|zero|impute|nan |
|
1982
|
|
|
|
|
|
|
impute_with => $impute_with, # mean|median (impute mode only) |
|
1983
|
|
|
|
|
|
|
missing_fill => undef, # per-feature fill, learned in fit() if impute |
|
1984
|
|
|
|
|
|
|
_use_c => $use_c, |
|
1985
|
|
|
|
|
|
|
_use_openmp => $use_openmp, |
|
1986
|
|
|
|
|
|
|
_use_openmp_fit => $use_openmp_fit, |
|
1987
|
|
|
|
|
|
|
threshold => undef, # learned in fit() if contamination set |
|
1988
|
|
|
|
|
|
|
trees => [], |
|
1989
|
|
|
|
|
|
|
c_psi => undef, # c(psi), set during fit() |
|
1990
|
|
|
|
|
|
|
n_features => undef, |
|
1991
|
|
|
|
|
|
|
feature_names => $args{feature_names}, # optional arrayref of per-feature labels |
|
1992
|
161
|
|
100
|
|
|
2213
|
}; |
|
|
|
|
100
|
|
|
|
|
|
1993
|
|
|
|
|
|
|
|
|
1994
|
161
|
100
|
|
|
|
621
|
croak "n_trees must be >= 1" unless $self->{n_trees} >= 1; |
|
1995
|
160
|
100
|
|
|
|
539
|
croak "sample_size must be >= 1" unless $self->{sample_size} >= 1; |
|
1996
|
|
|
|
|
|
|
croak "extension_level must be >= 0" |
|
1997
|
159
|
100
|
100
|
|
|
559
|
if defined $self->{extension_level} && $self->{extension_level} < 0; |
|
1998
|
|
|
|
|
|
|
croak "contamination must be a number in (0, 0.5]" |
|
1999
|
|
|
|
|
|
|
if defined $self->{contamination} |
|
2000
|
158
|
100
|
100
|
|
|
805
|
&& !( $self->{contamination} > 0 && $self->{contamination} <= 0.5 ); |
|
|
|
|
100
|
|
|
|
|
|
2001
|
|
|
|
|
|
|
croak "parallel_fit must be a positive integer" |
|
2002
|
|
|
|
|
|
|
if defined $self->{parallel_fit} |
|
2003
|
155
|
100
|
66
|
|
|
846
|
&& ( $self->{parallel_fit} !~ /^\d+$/ || $self->{parallel_fit} < 1 ); |
|
|
|
|
66
|
|
|
|
|
|
2004
|
|
|
|
|
|
|
|
|
2005
|
153
|
|
|
|
|
670
|
return bless $self, $class; |
|
2006
|
|
|
|
|
|
|
} ## end sub new |
|
2007
|
|
|
|
|
|
|
|
|
2008
|
|
|
|
|
|
|
=head2 decision_threshold |
|
2009
|
|
|
|
|
|
|
|
|
2010
|
|
|
|
|
|
|
The score cutoff C uses by default; undef unless C was |
|
2011
|
|
|
|
|
|
|
set. |
|
2012
|
|
|
|
|
|
|
|
|
2013
|
|
|
|
|
|
|
=cut |
|
2014
|
|
|
|
|
|
|
|
|
2015
|
6
|
|
|
6
|
1
|
3065
|
sub decision_threshold { return $_[0]->{threshold} } |
|
2016
|
|
|
|
|
|
|
|
|
2017
|
|
|
|
|
|
|
=head2 feature_names |
|
2018
|
|
|
|
|
|
|
|
|
2019
|
|
|
|
|
|
|
Returns the arrayref of feature name strings stored with the model, or undef |
|
2020
|
|
|
|
|
|
|
if none were provided at fit time. |
|
2021
|
|
|
|
|
|
|
|
|
2022
|
|
|
|
|
|
|
my $names = $iforest->feature_names; |
|
2023
|
|
|
|
|
|
|
|
|
2024
|
|
|
|
|
|
|
=cut |
|
2025
|
|
|
|
|
|
|
|
|
2026
|
0
|
|
|
0
|
1
|
0
|
sub feature_names { return $_[0]->{feature_names} } |
|
2027
|
|
|
|
|
|
|
|
|
2028
|
|
|
|
|
|
|
=head2 fit |
|
2029
|
|
|
|
|
|
|
|
|
2030
|
|
|
|
|
|
|
Trains the model on the specified data. |
|
2031
|
|
|
|
|
|
|
|
|
2032
|
|
|
|
|
|
|
The data taken is an array of arrays. Each sub-array is one sample and must |
|
2033
|
|
|
|
|
|
|
contain one or more numeric features. All samples must have the same number |
|
2034
|
|
|
|
|
|
|
of features. There is no upper limit on dimensionality. |
|
2035
|
|
|
|
|
|
|
|
|
2036
|
|
|
|
|
|
|
@training_data = ( |
|
2037
|
|
|
|
|
|
|
[ 3, 5 ], |
|
2038
|
|
|
|
|
|
|
[ 2.3, 1 ], |
|
2039
|
|
|
|
|
|
|
[ 5, 9 ], |
|
2040
|
|
|
|
|
|
|
... |
|
2041
|
|
|
|
|
|
|
); |
|
2042
|
|
|
|
|
|
|
|
|
2043
|
|
|
|
|
|
|
# Three-feature example |
|
2044
|
|
|
|
|
|
|
@training_data = ( |
|
2045
|
|
|
|
|
|
|
[ 1.0, 2.0, 3.0 ], |
|
2046
|
|
|
|
|
|
|
[ 1.1, 1.9, 3.1 ], |
|
2047
|
|
|
|
|
|
|
... |
|
2048
|
|
|
|
|
|
|
); |
|
2049
|
|
|
|
|
|
|
|
|
2050
|
|
|
|
|
|
|
Below shows an example of building a gaussian cluster and using that for training. |
|
2051
|
|
|
|
|
|
|
|
|
2052
|
|
|
|
|
|
|
# so it is reproducible |
|
2053
|
|
|
|
|
|
|
srand(7); |
|
2054
|
|
|
|
|
|
|
|
|
2055
|
|
|
|
|
|
|
# build a gaussian cluster and add a handful of outliers... |
|
2056
|
|
|
|
|
|
|
|
|
2057
|
|
|
|
|
|
|
use constant PI => 3.14159265358979; |
|
2058
|
|
|
|
|
|
|
sub gaussian { |
|
2059
|
|
|
|
|
|
|
my ($mu, $sigma) = @_; |
|
2060
|
|
|
|
|
|
|
my $u1 = rand() || 1e-12; |
|
2061
|
|
|
|
|
|
|
my $u2 = rand(); |
|
2062
|
|
|
|
|
|
|
my $z = sqrt(-2 * log($u1)) * cos(2 * PI * $u2); |
|
2063
|
|
|
|
|
|
|
return $mu + $sigma * $z; |
|
2064
|
|
|
|
|
|
|
} |
|
2065
|
|
|
|
|
|
|
|
|
2066
|
|
|
|
|
|
|
# add some normal items |
|
2067
|
|
|
|
|
|
|
for (1 .. 500) { |
|
2068
|
|
|
|
|
|
|
push @data, [ gaussian(0, 1), gaussian(0, 1) ]; |
|
2069
|
|
|
|
|
|
|
push @truth, 0; |
|
2070
|
|
|
|
|
|
|
} |
|
2071
|
|
|
|
|
|
|
# add some outliers |
|
2072
|
|
|
|
|
|
|
for (1 .. 20) { |
|
2073
|
|
|
|
|
|
|
my $angle = rand() * 2 * PI; |
|
2074
|
|
|
|
|
|
|
my $radius = 5 + rand() * 3; # distance 5..8 from the origin |
|
2075
|
|
|
|
|
|
|
push @data, [ $radius * cos($angle), $radius * sin($angle) ]; |
|
2076
|
|
|
|
|
|
|
push @truth, 1; |
|
2077
|
|
|
|
|
|
|
} |
|
2078
|
|
|
|
|
|
|
|
|
2079
|
|
|
|
|
|
|
$iforest->fit(\@data); |
|
2080
|
|
|
|
|
|
|
|
|
2081
|
|
|
|
|
|
|
=cut |
|
2082
|
|
|
|
|
|
|
|
|
2083
|
|
|
|
|
|
|
sub fit { |
|
2084
|
136
|
|
|
136
|
1
|
8261
|
my ( $self, $data ) = @_; |
|
2085
|
|
|
|
|
|
|
|
|
2086
|
136
|
100
|
100
|
|
|
1435
|
croak "fit() expects a non-empty arrayref of samples" |
|
2087
|
|
|
|
|
|
|
unless ref $data eq 'ARRAY' && @$data; |
|
2088
|
|
|
|
|
|
|
croak "each sample must be an arrayref of features" |
|
2089
|
130
|
100
|
100
|
|
|
660
|
unless ref $data->[0] eq 'ARRAY' && @{ $data->[0] }; |
|
|
128
|
|
|
|
|
675
|
|
|
2090
|
|
|
|
|
|
|
|
|
2091
|
126
|
|
|
|
|
216
|
my $n_features = scalar @{ $data->[0] }; |
|
|
126
|
|
|
|
|
241
|
|
|
2092
|
126
|
|
|
|
|
428
|
$self->{n_features} = $n_features; |
|
2093
|
|
|
|
|
|
|
|
|
2094
|
|
|
|
|
|
|
# Apply the missing-value strategy before any tree is built. Depending |
|
2095
|
|
|
|
|
|
|
# on the strategy this either croaks (die), returns a dense copy with |
|
2096
|
|
|
|
|
|
|
# undef cells filled (zero/impute), or passes the data through with |
|
2097
|
|
|
|
|
|
|
# undef preserved for the split logic to route (nan). Everything below |
|
2098
|
|
|
|
|
|
|
# trains on $train, never the raw $data. |
|
2099
|
126
|
|
|
|
|
603
|
my $train = $self->_prepare_fit_data($data); |
|
2100
|
|
|
|
|
|
|
|
|
2101
|
120
|
|
|
|
|
248
|
my $n = scalar @$train; |
|
2102
|
|
|
|
|
|
|
|
|
2103
|
|
|
|
|
|
|
# The sub-sample cannot be larger than the data set itself. |
|
2104
|
120
|
|
|
|
|
595
|
my $psi = min( $self->{sample_size}, $n ); |
|
2105
|
120
|
|
|
|
|
437
|
$self->{c_psi} = _c($psi); |
|
2106
|
120
|
|
|
|
|
304
|
$self->{psi_used} = $psi; |
|
2107
|
|
|
|
|
|
|
|
|
2108
|
|
|
|
|
|
|
# Resolve the extension level against the data's dimensionality. |
|
2109
|
120
|
100
|
|
|
|
340
|
if ( $self->{mode} eq 'extended' ) { |
|
2110
|
29
|
|
|
|
|
55
|
my $max_ext = $n_features - 1; |
|
2111
|
|
|
|
|
|
|
my $ext |
|
2112
|
|
|
|
|
|
|
= defined $self->{extension_level} |
|
2113
|
|
|
|
|
|
|
? $self->{extension_level} |
|
2114
|
29
|
100
|
|
|
|
104
|
: $max_ext; |
|
2115
|
29
|
50
|
|
|
|
82
|
$ext = 0 if $ext < 0; |
|
2116
|
29
|
100
|
|
|
|
72
|
$ext = $max_ext if $ext > $max_ext; |
|
2117
|
29
|
|
|
|
|
63
|
$self->{extension_level_used} = $ext; |
|
2118
|
|
|
|
|
|
|
} else { |
|
2119
|
91
|
|
|
|
|
182
|
$self->{extension_level_used} = undef; |
|
2120
|
|
|
|
|
|
|
} |
|
2121
|
|
|
|
|
|
|
|
|
2122
|
|
|
|
|
|
|
# Height limit: the average tree height ceil(log2(psi)). Past this depth the |
|
2123
|
|
|
|
|
|
|
# remaining points are scored using the c(size) adjustment instead. |
|
2124
|
|
|
|
|
|
|
my $limit |
|
2125
|
|
|
|
|
|
|
= defined $self->{max_depth} |
|
2126
|
|
|
|
|
|
|
? $self->{max_depth} |
|
2127
|
120
|
50
|
|
|
|
749
|
: ceil( log($psi) / log(2) ); |
|
2128
|
120
|
50
|
|
|
|
339
|
$limit = 1 if $limit < 1; |
|
2129
|
120
|
|
|
|
|
362
|
$self->{max_depth_used} = $limit; |
|
2130
|
|
|
|
|
|
|
|
|
2131
|
120
|
50
|
|
|
|
509
|
srand( $self->{seed} ) if defined $self->{seed}; |
|
2132
|
|
|
|
|
|
|
|
|
2133
|
120
|
|
|
|
|
258
|
my $workers = $self->{parallel_fit}; |
|
2134
|
120
|
100
|
100
|
|
|
962
|
if ( defined $workers |
|
|
|
100
|
66
|
|
|
|
|
|
|
|
100
|
66
|
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
2135
|
|
|
|
|
|
|
&& $workers > 1 |
|
2136
|
|
|
|
|
|
|
&& $self->{n_trees} > 1 |
|
2137
|
|
|
|
|
|
|
&& _fork_supported() ) |
|
2138
|
|
|
|
|
|
|
{ |
|
2139
|
8
|
|
|
|
|
45
|
$self->{trees} = $self->_fit_trees_parallel( $train, $psi, $limit, $workers ); |
|
2140
|
|
|
|
|
|
|
} elsif ( $self->{_use_c} && $self->{_use_openmp_fit} ) { |
|
2141
|
7
|
|
|
|
|
39
|
$self->{trees} = $self->_build_forest_openmp( $train, $psi, $limit, $self->{n_trees} ); |
|
2142
|
|
|
|
|
|
|
} elsif ( $self->{_use_c} ) { |
|
2143
|
|
|
|
|
|
|
$self->{trees} |
|
2144
|
58
|
|
|
|
|
335
|
= $self->_build_forest_c( $train, $psi, $limit, $self->{n_trees} ); |
|
2145
|
|
|
|
|
|
|
} else { |
|
2146
|
47
|
|
|
|
|
75
|
my @trees; |
|
2147
|
47
|
|
|
|
|
129
|
for ( 1 .. $self->{n_trees} ) { |
|
2148
|
2169
|
|
|
|
|
5401
|
my $sample = _subsample( $train, $psi ); |
|
2149
|
2169
|
|
|
|
|
5658
|
push @trees, $self->_build_tree( $sample, 0, $limit ); |
|
2150
|
|
|
|
|
|
|
} |
|
2151
|
47
|
|
|
|
|
204
|
$self->{trees} = \@trees; |
|
2152
|
|
|
|
|
|
|
} |
|
2153
|
|
|
|
|
|
|
|
|
2154
|
|
|
|
|
|
|
# On a re-fit, packed scoring buffers from the previous fit are still |
|
2155
|
|
|
|
|
|
|
# sitting on the object; score_samples() below would pick them up and |
|
2156
|
|
|
|
|
|
|
# learn the contamination threshold against the OLD forest. Drop them |
|
2157
|
|
|
|
|
|
|
# so the training-set scoring runs pure-Perl against the trees just |
|
2158
|
|
|
|
|
|
|
# built; _rebuild_c_trees repacks from the new trees at the end. |
|
2159
|
120
|
|
|
|
|
701
|
delete @$self{qw(_c_nodes _c_coef_idx _c_coef_val)}; |
|
2160
|
|
|
|
|
|
|
|
|
2161
|
|
|
|
|
|
|
# If a contamination rate was requested, learn the score cutoff that flags |
|
2162
|
|
|
|
|
|
|
# that fraction of the training set. We place the threshold midway between |
|
2163
|
|
|
|
|
|
|
# the k-th and (k+1)-th highest training scores, so it sits in the gap |
|
2164
|
|
|
|
|
|
|
# between flagged and unflagged points -- unambiguous and robust to the |
|
2165
|
|
|
|
|
|
|
# tiny float rounding introduced by JSON serialisation. |
|
2166
|
120
|
100
|
|
|
|
508
|
if ( defined $self->{contamination} ) { |
|
2167
|
3
|
|
|
|
|
15
|
my $scores = $self->score_samples($train); |
|
2168
|
3
|
|
|
|
|
45
|
my @desc = sort { $b <=> $a } @$scores; |
|
|
3979
|
|
|
|
|
4366
|
|
|
2169
|
3
|
|
|
|
|
11
|
my $n_pts = scalar @desc; |
|
2170
|
3
|
|
|
|
|
19
|
my $k = int( $self->{contamination} * $n_pts + 0.5 ); |
|
2171
|
3
|
50
|
|
|
|
12
|
$k = 1 if $k < 1; |
|
2172
|
3
|
50
|
|
|
|
8
|
$k = $n_pts if $k > $n_pts; |
|
2173
|
3
|
50
|
|
|
|
44
|
$self->{threshold} = $k < $n_pts |
|
2174
|
|
|
|
|
|
|
? ( $desc[ $k - 1 ] + $desc[$k] ) / 2.0 # midpoint of the boundary |
|
2175
|
|
|
|
|
|
|
: $desc[ $n_pts - 1 ] - 1e-9; # k == n: flag everything |
|
2176
|
|
|
|
|
|
|
} ## end if ( defined $self->{contamination} ) |
|
2177
|
|
|
|
|
|
|
|
|
2178
|
120
|
100
|
|
|
|
641
|
$self->_rebuild_c_trees() if $self->{_use_c}; |
|
2179
|
120
|
|
|
|
|
1289
|
return $self; |
|
2180
|
|
|
|
|
|
|
} ## end sub fit |
|
2181
|
|
|
|
|
|
|
|
|
2182
|
|
|
|
|
|
|
=head2 pack_data(\@data) |
|
2183
|
|
|
|
|
|
|
|
|
2184
|
|
|
|
|
|
|
Returns an opaque, blessed wrapper around the input dataset that the |
|
2185
|
|
|
|
|
|
|
scoring methods can use directly, skipping the per-call work of walking |
|
2186
|
|
|
|
|
|
|
the arrayref-of-arrayrefs and converting each cell into a double. At |
|
2187
|
|
|
|
|
|
|
high feature counts this is a meaningful win when the same dataset is |
|
2188
|
|
|
|
|
|
|
scored repeatedly (e.g. interactive threshold tuning, dashboards, |
|
2189
|
|
|
|
|
|
|
plotting that updates as parameters change). |
|
2190
|
|
|
|
|
|
|
|
|
2191
|
|
|
|
|
|
|
Requires the Inline::C backend; croaks if C is false. |
|
2192
|
|
|
|
|
|
|
|
|
2193
|
|
|
|
|
|
|
my $packed = $forest->pack_data(\@data); |
|
2194
|
|
|
|
|
|
|
|
|
2195
|
|
|
|
|
|
|
# Now any of these accept either an arrayref or the packed wrapper: |
|
2196
|
|
|
|
|
|
|
my $scores = $forest->score_samples($packed); |
|
2197
|
|
|
|
|
|
|
my $flags = $forest->predict($packed, 0.6); |
|
2198
|
|
|
|
|
|
|
my ($s, $l) = $forest->score_predict_split($packed); |
|
2199
|
|
|
|
|
|
|
|
|
2200
|
|
|
|
|
|
|
The wrapper has C and C accessors for introspection. |
|
2201
|
|
|
|
|
|
|
The feature count is matched against the model on every call; passing a |
|
2202
|
|
|
|
|
|
|
packed dataset built for a different feature count is a fatal error. |
|
2203
|
|
|
|
|
|
|
|
|
2204
|
|
|
|
|
|
|
=cut |
|
2205
|
|
|
|
|
|
|
|
|
2206
|
|
|
|
|
|
|
=head2 path_lengths(\@data) |
|
2207
|
|
|
|
|
|
|
|
|
2208
|
|
|
|
|
|
|
Returns an arrayref of the mean isolation depth per sample, for inspection. |
|
2209
|
|
|
|
|
|
|
|
|
2210
|
|
|
|
|
|
|
my $lengths = $forest->path_lengths(\@data); |
|
2211
|
|
|
|
|
|
|
|
|
2212
|
|
|
|
|
|
|
print "x, y, length\n"; |
|
2213
|
|
|
|
|
|
|
|
|
2214
|
|
|
|
|
|
|
my $int=0; |
|
2215
|
|
|
|
|
|
|
while (defined($data[$int])) { |
|
2216
|
|
|
|
|
|
|
print $data[$int][0].', '.$data[$int][1].', '.$lengths->[$int]."\n"; |
|
2217
|
|
|
|
|
|
|
|
|
2218
|
|
|
|
|
|
|
$int++; |
|
2219
|
|
|
|
|
|
|
} |
|
2220
|
|
|
|
|
|
|
|
|
2221
|
|
|
|
|
|
|
=cut |
|
2222
|
|
|
|
|
|
|
|
|
2223
|
|
|
|
|
|
|
sub path_lengths { |
|
2224
|
7508
|
|
|
7508
|
1
|
26841
|
my ( $self, $data ) = @_; |
|
2225
|
7508
|
|
|
|
|
18372
|
$self->_check_fitted; |
|
2226
|
7506
|
|
|
|
|
12994
|
my $trees = $self->{trees}; |
|
2227
|
7506
|
|
|
|
|
12122
|
my $t = scalar @$trees; |
|
2228
|
|
|
|
|
|
|
|
|
2229
|
7506
|
50
|
66
|
|
|
24274
|
if ( $self->{_use_c} && $self->{_c_nodes} ) { |
|
2230
|
7505
|
|
|
|
|
15053
|
my ( $n_pts, $nf, $x_packed ) = $self->_resolve_input($data); |
|
2231
|
7505
|
|
|
|
|
14388
|
my $sums_packed = "\0" x ( $n_pts * 8 ); |
|
2232
|
|
|
|
|
|
|
score_all_xs( |
|
2233
|
|
|
|
|
|
|
$self->{_c_nodes}, $self->{_c_coef_idx}, $self->{_c_coef_val}, |
|
2234
|
|
|
|
|
|
|
$x_packed, $sums_packed, $n_pts, |
|
2235
|
|
|
|
|
|
|
$nf, $t, $self->{_use_openmp} |
|
2236
|
7505
|
|
|
|
|
164785
|
); |
|
2237
|
7505
|
|
|
|
|
13170
|
my $result = []; |
|
2238
|
7505
|
|
|
|
|
25925
|
finalize_path_lengths_xs( $sums_packed, $n_pts, $t + 0.0, $result ); |
|
2239
|
7505
|
|
|
|
|
28220
|
return $result; |
|
2240
|
|
|
|
|
|
|
} ## end if ( $self->{_use_c} && $self->{_c_nodes} ) |
|
2241
|
|
|
|
|
|
|
|
|
2242
|
1
|
|
|
|
|
4
|
$data = $self->_prepare_perl_input($data); |
|
2243
|
1
|
50
|
|
|
|
4
|
my $nan = $self->{missing} eq 'nan' ? 1 : 0; |
|
2244
|
|
|
|
|
|
|
|
|
2245
|
|
|
|
|
|
|
# Pure-Perl fallback (tree-outer, sample-inner for cache locality). |
|
2246
|
1
|
|
|
|
|
5
|
my @sums = (0) x @$data; |
|
2247
|
1
|
|
|
|
|
2
|
for my $tree (@$trees) { |
|
2248
|
60
|
|
|
|
|
87
|
for my $i ( 0 .. $#$data ) { |
|
2249
|
6000
|
|
|
|
|
7615
|
$sums[$i] += _path_length( $data->[$i], $tree, 0, $nan ); |
|
2250
|
|
|
|
|
|
|
} |
|
2251
|
|
|
|
|
|
|
} |
|
2252
|
1
|
|
|
|
|
10
|
return [ map { $_ / $t } @sums ]; |
|
|
100
|
|
|
|
|
131
|
|
|
2253
|
|
|
|
|
|
|
} ## end sub path_lengths |
|
2254
|
|
|
|
|
|
|
|
|
2255
|
|
|
|
|
|
|
=head2 predict(\@data, $threshold) |
|
2256
|
|
|
|
|
|
|
|
|
2257
|
|
|
|
|
|
|
Returns an arrayref of 0/1 labels for the specified data. |
|
2258
|
|
|
|
|
|
|
|
|
2259
|
|
|
|
|
|
|
If threshold is not specified it uses the contamination-learned cutoff (if |
|
2260
|
|
|
|
|
|
|
C was called with C), otherwise 0.5. |
|
2261
|
|
|
|
|
|
|
|
|
2262
|
|
|
|
|
|
|
my $results = $forest->predict(\@data, $threshold); |
|
2263
|
|
|
|
|
|
|
|
|
2264
|
|
|
|
|
|
|
print "x, y, result\n"; |
|
2265
|
|
|
|
|
|
|
|
|
2266
|
|
|
|
|
|
|
my $int=0; |
|
2267
|
|
|
|
|
|
|
while (defined($data[$int])) { |
|
2268
|
|
|
|
|
|
|
print $data[$int][0].', '.$data[$int][1].', '.$results->[$int]."\n"; |
|
2269
|
|
|
|
|
|
|
|
|
2270
|
|
|
|
|
|
|
$int++; |
|
2271
|
|
|
|
|
|
|
} |
|
2272
|
|
|
|
|
|
|
|
|
2273
|
|
|
|
|
|
|
=cut |
|
2274
|
|
|
|
|
|
|
|
|
2275
|
|
|
|
|
|
|
sub predict { |
|
2276
|
15007
|
|
|
15007
|
1
|
88887
|
my ( $self, $data, $threshold ) = @_; |
|
2277
|
|
|
|
|
|
|
$threshold |
|
2278
|
|
|
|
|
|
|
= defined $threshold ? $threshold |
|
2279
|
|
|
|
|
|
|
: defined $self->{threshold} ? $self->{threshold} |
|
2280
|
15007
|
100
|
|
|
|
26414
|
: 0.5; |
|
|
|
100
|
|
|
|
|
|
|
2281
|
15007
|
|
|
|
|
34432
|
$self->_check_fitted; |
|
2282
|
|
|
|
|
|
|
|
|
2283
|
|
|
|
|
|
|
# Fast path: threshold the raw path-length sums directly, skipping the |
|
2284
|
|
|
|
|
|
|
# per-point exp() and the intermediate scores arrayref. |
|
2285
|
|
|
|
|
|
|
# Derivation: score = exp(-sum * log(2) / (c*t)) |
|
2286
|
|
|
|
|
|
|
# so score >= T iff sum <= -log(T) * c * t / log(2) |
|
2287
|
|
|
|
|
|
|
# Only valid for a normal threshold in (0, 1) and a positive c. |
|
2288
|
15005
|
100
|
66
|
|
|
90964
|
if ( $self->{_use_c} |
|
|
|
|
33
|
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
2289
|
|
|
|
|
|
|
&& $self->{_c_nodes} |
|
2290
|
|
|
|
|
|
|
&& $self->{c_psi} > 0 |
|
2291
|
|
|
|
|
|
|
&& $threshold > 0 |
|
2292
|
|
|
|
|
|
|
&& $threshold < 1 ) |
|
2293
|
|
|
|
|
|
|
{ |
|
2294
|
14988
|
|
|
|
|
24443
|
my $trees = $self->{trees}; |
|
2295
|
14988
|
|
|
|
|
22557
|
my $t = scalar @$trees; |
|
2296
|
14988
|
|
|
|
|
23349
|
my $c = $self->{c_psi}; |
|
2297
|
14988
|
|
|
|
|
29168
|
my ( $n_pts, $nf, $x_packed ) = $self->_resolve_input($data); |
|
2298
|
14988
|
|
|
|
|
28142
|
my $sums_packed = "\0" x ( $n_pts * 8 ); |
|
2299
|
|
|
|
|
|
|
score_all_xs( |
|
2300
|
|
|
|
|
|
|
$self->{_c_nodes}, $self->{_c_coef_idx}, $self->{_c_coef_val}, |
|
2301
|
|
|
|
|
|
|
$x_packed, $sums_packed, $n_pts, |
|
2302
|
|
|
|
|
|
|
$nf, $t, $self->{_use_openmp} |
|
2303
|
14988
|
|
|
|
|
378510
|
); |
|
2304
|
14988
|
|
|
|
|
35356
|
my $sum_threshold = -log($threshold) * $c * $t / log(2); |
|
2305
|
14988
|
|
|
|
|
23134
|
my $result = []; |
|
2306
|
14988
|
|
|
|
|
46407
|
predict_sums_xs( $sums_packed, $n_pts, $sum_threshold, $result ); |
|
2307
|
14988
|
|
|
|
|
57411
|
return $result; |
|
2308
|
|
|
|
|
|
|
} ## end if ( $self->{_use_c} && $self->{_c_nodes} ...) |
|
2309
|
|
|
|
|
|
|
|
|
2310
|
|
|
|
|
|
|
# Fallback: edge thresholds, c==0, or no C backend. |
|
2311
|
17
|
|
|
|
|
92
|
my $scores = $self->score_samples( $self->_to_arrayref($data) ); |
|
2312
|
17
|
100
|
|
|
|
65
|
return [ map { $_ >= $threshold ? 1 : 0 } @$scores ]; |
|
|
1285
|
|
|
|
|
2359
|
|
|
2313
|
|
|
|
|
|
|
} ## end sub predict |
|
2314
|
|
|
|
|
|
|
|
|
2315
|
|
|
|
|
|
|
=head2 predict_tagged(\%row, $threshold) |
|
2316
|
|
|
|
|
|
|
|
|
2317
|
|
|
|
|
|
|
Predicts whether a single sample is an anomaly using a hashref of named |
|
2318
|
|
|
|
|
|
|
feature values. The model must have been fitted (or loaded from a model |
|
2319
|
|
|
|
|
|
|
that was fitted) with feature names stored via C. |
|
2320
|
|
|
|
|
|
|
|
|
2321
|
|
|
|
|
|
|
C<$threshold> defaults the same way as in C. |
|
2322
|
|
|
|
|
|
|
|
|
2323
|
|
|
|
|
|
|
Returns a scalar 1 (anomaly) or 0 (normal). |
|
2324
|
|
|
|
|
|
|
|
|
2325
|
|
|
|
|
|
|
my $label = $forest->predict_tagged( |
|
2326
|
|
|
|
|
|
|
{ cpu => 0.9, mem => 0.4, disk => 0.1 }, |
|
2327
|
|
|
|
|
|
|
); |
|
2328
|
|
|
|
|
|
|
|
|
2329
|
|
|
|
|
|
|
Croaks if the model has no stored feature names, if the hashref contains a |
|
2330
|
|
|
|
|
|
|
key that is not a known feature name, or if a feature name is absent from the |
|
2331
|
|
|
|
|
|
|
hashref. |
|
2332
|
|
|
|
|
|
|
|
|
2333
|
|
|
|
|
|
|
=cut |
|
2334
|
|
|
|
|
|
|
|
|
2335
|
|
|
|
|
|
|
=head2 tagged_row_to_array(\%row, $caller) |
|
2336
|
|
|
|
|
|
|
|
|
2337
|
|
|
|
|
|
|
Validates a hashref of named feature values against the model's stored |
|
2338
|
|
|
|
|
|
|
C and returns a positional arrayref ready to pass to any |
|
2339
|
|
|
|
|
|
|
of the scoring or prediction methods. |
|
2340
|
|
|
|
|
|
|
|
|
2341
|
|
|
|
|
|
|
C<$caller> is a string used in error messages to identify which method |
|
2342
|
|
|
|
|
|
|
triggered the validation (pass the calling method's name). |
|
2343
|
|
|
|
|
|
|
|
|
2344
|
|
|
|
|
|
|
my $vec = $forest->tagged_row_to_array(\%row, 'my_method'); |
|
2345
|
|
|
|
|
|
|
# returns e.g. [0.9, 0.4, 0.1] ordered by feature_names |
|
2346
|
|
|
|
|
|
|
|
|
2347
|
|
|
|
|
|
|
Croaks if: |
|
2348
|
|
|
|
|
|
|
|
|
2349
|
|
|
|
|
|
|
=over 4 |
|
2350
|
|
|
|
|
|
|
|
|
2351
|
|
|
|
|
|
|
=item * C<$row> is not a hashref |
|
2352
|
|
|
|
|
|
|
|
|
2353
|
|
|
|
|
|
|
=item * the model has no stored C |
|
2354
|
|
|
|
|
|
|
|
|
2355
|
|
|
|
|
|
|
=item * the hashref contains a key that is not a known feature name |
|
2356
|
|
|
|
|
|
|
|
|
2357
|
|
|
|
|
|
|
=item * a feature name is absent from the hashref |
|
2358
|
|
|
|
|
|
|
|
|
2359
|
|
|
|
|
|
|
=back |
|
2360
|
|
|
|
|
|
|
|
|
2361
|
|
|
|
|
|
|
=cut |
|
2362
|
|
|
|
|
|
|
|
|
2363
|
|
|
|
|
|
|
sub tagged_row_to_array { |
|
2364
|
0
|
|
|
0
|
1
|
0
|
my ( $self, $row, $caller ) = @_; |
|
2365
|
0
|
0
|
|
|
|
0
|
croak "$caller requires a hashref" |
|
2366
|
|
|
|
|
|
|
unless ref $row eq 'HASH'; |
|
2367
|
|
|
|
|
|
|
croak "this model has no stored feature_names; " . "refit with -t tags or pass feature_names to new()" |
|
2368
|
|
|
|
|
|
|
unless defined $self->{feature_names} |
|
2369
|
|
|
|
|
|
|
&& ref $self->{feature_names} eq 'ARRAY' |
|
2370
|
0
|
0
|
0
|
|
|
0
|
&& @{ $self->{feature_names} }; |
|
|
0
|
|
0
|
|
|
0
|
|
|
2371
|
|
|
|
|
|
|
|
|
2372
|
0
|
|
|
|
|
0
|
my @names = @{ $self->{feature_names} }; |
|
|
0
|
|
|
|
|
0
|
|
|
2373
|
|
|
|
|
|
|
|
|
2374
|
|
|
|
|
|
|
my @unknown = grep { |
|
2375
|
0
|
|
|
|
|
0
|
my $k = $_; |
|
|
0
|
|
|
|
|
0
|
|
|
2376
|
0
|
|
|
|
|
0
|
!grep { $_ eq $k } @names |
|
|
0
|
|
|
|
|
0
|
|
|
2377
|
|
|
|
|
|
|
} keys %$row; |
|
2378
|
0
|
0
|
|
|
|
0
|
croak "unknown feature name(s) in hashref: " . join( ', ', sort @unknown ) |
|
2379
|
|
|
|
|
|
|
if @unknown; |
|
2380
|
|
|
|
|
|
|
|
|
2381
|
0
|
|
|
|
|
0
|
my @missing = grep { !exists $row->{$_} } @names; |
|
|
0
|
|
|
|
|
0
|
|
|
2382
|
0
|
0
|
|
|
|
0
|
croak "missing feature name(s) in hashref: " . join( ', ', @missing ) |
|
2383
|
|
|
|
|
|
|
if @missing; |
|
2384
|
|
|
|
|
|
|
|
|
2385
|
0
|
|
|
|
|
0
|
return [ map { $row->{$_} } @names ]; |
|
|
0
|
|
|
|
|
0
|
|
|
2386
|
|
|
|
|
|
|
} ## end sub tagged_row_to_array |
|
2387
|
|
|
|
|
|
|
|
|
2388
|
|
|
|
|
|
|
sub predict_tagged { |
|
2389
|
0
|
|
|
0
|
1
|
0
|
my ( $self, $row, $threshold ) = @_; |
|
2390
|
0
|
|
|
|
|
0
|
my $vec = $self->tagged_row_to_array( $row, 'predict_tagged' ); |
|
2391
|
0
|
|
|
|
|
0
|
my $result = $self->predict( [$vec], $threshold ); |
|
2392
|
0
|
|
|
|
|
0
|
return $result->[0]; |
|
2393
|
|
|
|
|
|
|
} |
|
2394
|
|
|
|
|
|
|
|
|
2395
|
|
|
|
|
|
|
=head2 score_samples(\@data) |
|
2396
|
|
|
|
|
|
|
|
|
2397
|
|
|
|
|
|
|
Returns an arrayref of anomaly scores, between 0 and 1. |
|
2398
|
|
|
|
|
|
|
|
|
2399
|
|
|
|
|
|
|
Scores near 1 are strong anomalies (isolated quickly). |
|
2400
|
|
|
|
|
|
|
|
|
2401
|
|
|
|
|
|
|
Scores well below 0.5 are normal. |
|
2402
|
|
|
|
|
|
|
|
|
2403
|
|
|
|
|
|
|
Scores ~0.5 means the points are hard to tell apart. |
|
2404
|
|
|
|
|
|
|
|
|
2405
|
|
|
|
|
|
|
my $scores = $forest->score_samples(\@data); |
|
2406
|
|
|
|
|
|
|
|
|
2407
|
|
|
|
|
|
|
print "x, y, score\n"; |
|
2408
|
|
|
|
|
|
|
|
|
2409
|
|
|
|
|
|
|
my $int=0; |
|
2410
|
|
|
|
|
|
|
while (defined($data[$int])) { |
|
2411
|
|
|
|
|
|
|
print $data[$int][0].', '.$data[$int][1].', '.$scores->[$int]."\n"; |
|
2412
|
|
|
|
|
|
|
|
|
2413
|
|
|
|
|
|
|
$int++; |
|
2414
|
|
|
|
|
|
|
} |
|
2415
|
|
|
|
|
|
|
|
|
2416
|
|
|
|
|
|
|
=cut |
|
2417
|
|
|
|
|
|
|
|
|
2418
|
|
|
|
|
|
|
sub score_samples { |
|
2419
|
14268
|
|
|
14268
|
1
|
91845
|
my ( $self, $data ) = @_; |
|
2420
|
14268
|
|
|
|
|
34835
|
$self->_check_fitted; |
|
2421
|
14266
|
|
|
|
|
25532
|
my $c = $self->{c_psi}; |
|
2422
|
14266
|
|
|
|
|
23197
|
my $trees = $self->{trees}; |
|
2423
|
14266
|
|
|
|
|
23513
|
my $t = scalar @$trees; |
|
2424
|
|
|
|
|
|
|
|
|
2425
|
14266
|
100
|
100
|
|
|
46534
|
if ( $self->{_use_c} && $self->{_c_nodes} ) { |
|
2426
|
14208
|
|
|
|
|
30270
|
my ( $n_pts, $nf, $x_packed ) = $self->_resolve_input($data); |
|
2427
|
14207
|
|
|
|
|
28089
|
my $sums_packed = "\0" x ( $n_pts * 8 ); |
|
2428
|
|
|
|
|
|
|
score_all_xs( |
|
2429
|
|
|
|
|
|
|
$self->{_c_nodes}, $self->{_c_coef_idx}, $self->{_c_coef_val}, |
|
2430
|
|
|
|
|
|
|
$x_packed, $sums_packed, $n_pts, |
|
2431
|
|
|
|
|
|
|
$nf, $t, $self->{_use_openmp} |
|
2432
|
14207
|
|
|
|
|
526233
|
); |
|
2433
|
14207
|
50
|
|
|
|
36206
|
if ( $c > 0 ) { |
|
2434
|
14207
|
|
|
|
|
28613
|
my $inv = log(2) / ( $c * $t ); |
|
2435
|
14207
|
|
|
|
|
22266
|
my $result = []; |
|
2436
|
14207
|
|
|
|
|
48518
|
finalize_scores_xs( $sums_packed, $n_pts, $inv, $result ); |
|
2437
|
14207
|
|
|
|
|
57659
|
return $result; |
|
2438
|
|
|
|
|
|
|
} |
|
2439
|
0
|
|
|
|
|
0
|
return [ (0.5) x $n_pts ]; |
|
2440
|
|
|
|
|
|
|
} ## end if ( $self->{_use_c} && $self->{_c_nodes} ) |
|
2441
|
|
|
|
|
|
|
|
|
2442
|
58
|
|
|
|
|
308
|
$data = $self->_prepare_perl_input($data); |
|
2443
|
58
|
100
|
|
|
|
184
|
my $nan = $self->{missing} eq 'nan' ? 1 : 0; |
|
2444
|
|
|
|
|
|
|
|
|
2445
|
|
|
|
|
|
|
# Pure-Perl fallback (tree-outer, sample-inner for cache locality). |
|
2446
|
58
|
|
|
|
|
381
|
my @sums = (0) x @$data; |
|
2447
|
58
|
|
|
|
|
153
|
for my $tree (@$trees) { |
|
2448
|
4302
|
|
|
|
|
7577
|
for my $i ( 0 .. $#$data ) { |
|
2449
|
334276
|
|
|
|
|
451522
|
$sums[$i] += _path_length( $data->[$i], $tree, 0, $nan ); |
|
2450
|
|
|
|
|
|
|
} |
|
2451
|
|
|
|
|
|
|
} |
|
2452
|
|
|
|
|
|
|
|
|
2453
|
|
|
|
|
|
|
# Precompute the single normalising factor; exp() is a direct FPU |
|
2454
|
|
|
|
|
|
|
# instruction and faster than Perl's general-purpose 2**x (pow). |
|
2455
|
|
|
|
|
|
|
# Derivation: 2**(-avg/c) = 2**(-(sum/t)/c) = exp(-sum * log(2)/(c*t)) |
|
2456
|
58
|
50
|
|
|
|
247
|
if ( $c > 0 ) { |
|
2457
|
58
|
|
|
|
|
125
|
my $inv = log(2) / ( $c * $t ); |
|
2458
|
58
|
|
|
|
|
239
|
return [ map { exp( -$_ * $inv ) } @sums ]; |
|
|
4532
|
|
|
|
|
6931
|
|
|
2459
|
|
|
|
|
|
|
} |
|
2460
|
0
|
|
|
|
|
0
|
return [ (0.5) x @sums ]; |
|
2461
|
|
|
|
|
|
|
} ## end sub score_samples |
|
2462
|
|
|
|
|
|
|
|
|
2463
|
|
|
|
|
|
|
=head2 score_sample_tagged(\%row) |
|
2464
|
|
|
|
|
|
|
|
|
2465
|
|
|
|
|
|
|
Scores a single sample supplied as a hashref of named feature values. |
|
2466
|
|
|
|
|
|
|
The model must have stored feature names (set via C in |
|
2467
|
|
|
|
|
|
|
C or the C<-t> CLI flag at fit time). |
|
2468
|
|
|
|
|
|
|
|
|
2469
|
|
|
|
|
|
|
Returns a scalar anomaly score in (0, 1]. |
|
2470
|
|
|
|
|
|
|
|
|
2471
|
|
|
|
|
|
|
my $score = $forest->score_sample_tagged({ cpu => 0.9, mem => 0.4 }); |
|
2472
|
|
|
|
|
|
|
|
|
2473
|
|
|
|
|
|
|
Croaks if the model has no stored feature names, if the hashref contains a |
|
2474
|
|
|
|
|
|
|
key that is not a known feature name, or if a feature name is absent from the |
|
2475
|
|
|
|
|
|
|
hashref. |
|
2476
|
|
|
|
|
|
|
|
|
2477
|
|
|
|
|
|
|
=cut |
|
2478
|
|
|
|
|
|
|
|
|
2479
|
|
|
|
|
|
|
sub score_sample_tagged { |
|
2480
|
0
|
|
|
0
|
1
|
0
|
my ( $self, $row ) = @_; |
|
2481
|
0
|
|
|
|
|
0
|
my $vec = $self->tagged_row_to_array( $row, 'score_sample_tagged' ); |
|
2482
|
0
|
|
|
|
|
0
|
my $result = $self->score_samples( [$vec] ); |
|
2483
|
0
|
|
|
|
|
0
|
return $result->[0]; |
|
2484
|
|
|
|
|
|
|
} |
|
2485
|
|
|
|
|
|
|
|
|
2486
|
|
|
|
|
|
|
=head2 score_predict_samples |
|
2487
|
|
|
|
|
|
|
|
|
2488
|
|
|
|
|
|
|
Returns an array ref of arrays. First value of each sub array is the score with the second being |
|
2489
|
|
|
|
|
|
|
0/1 for if it is a anomaly or not. |
|
2490
|
|
|
|
|
|
|
|
|
2491
|
|
|
|
|
|
|
C<$threshold> defaults the same way as in C. |
|
2492
|
|
|
|
|
|
|
|
|
2493
|
|
|
|
|
|
|
my $results = $forest->score_predict_samples(\@data, $threshold); |
|
2494
|
|
|
|
|
|
|
|
|
2495
|
|
|
|
|
|
|
print "x, y, score, result\n"; |
|
2496
|
|
|
|
|
|
|
|
|
2497
|
|
|
|
|
|
|
my $int=0; |
|
2498
|
|
|
|
|
|
|
while (defined($data[$int])) { |
|
2499
|
|
|
|
|
|
|
print $data[$int][0].', '.$data[$int][1].', '.$results->[$int][0].', '.$results->[$int][1]."\n"; |
|
2500
|
|
|
|
|
|
|
|
|
2501
|
|
|
|
|
|
|
$int++; |
|
2502
|
|
|
|
|
|
|
} |
|
2503
|
|
|
|
|
|
|
|
|
2504
|
|
|
|
|
|
|
=cut |
|
2505
|
|
|
|
|
|
|
|
|
2506
|
|
|
|
|
|
|
sub score_predict_samples { |
|
2507
|
5105
|
|
|
5105
|
1
|
19114
|
my ( $self, $data, $threshold ) = @_; |
|
2508
|
|
|
|
|
|
|
$threshold |
|
2509
|
|
|
|
|
|
|
= defined $threshold ? $threshold |
|
2510
|
|
|
|
|
|
|
: defined $self->{threshold} ? $self->{threshold} |
|
2511
|
5105
|
50
|
|
|
|
9636
|
: 0.5; |
|
|
|
100
|
|
|
|
|
|
|
2512
|
5105
|
|
|
|
|
12620
|
$self->_check_fitted; |
|
2513
|
|
|
|
|
|
|
|
|
2514
|
|
|
|
|
|
|
# Fast path: build [score, label] pairs straight from the sum buffer |
|
2515
|
|
|
|
|
|
|
# in one C call. Avoids the intermediate scores arrayref + Perl |
|
2516
|
|
|
|
|
|
|
# foreach that allocates ~3*n_pts SVs. Gated identically to predict() |
|
2517
|
|
|
|
|
|
|
# so the threshold conversion is valid. |
|
2518
|
5105
|
50
|
66
|
|
|
33720
|
if ( $self->{_use_c} |
|
|
|
|
33
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
2519
|
|
|
|
|
|
|
&& $self->{_c_nodes} |
|
2520
|
|
|
|
|
|
|
&& $self->{c_psi} > 0 |
|
2521
|
|
|
|
|
|
|
&& $threshold > 0 |
|
2522
|
|
|
|
|
|
|
&& $threshold < 1 ) |
|
2523
|
|
|
|
|
|
|
{ |
|
2524
|
5103
|
|
|
|
|
8528
|
my $trees = $self->{trees}; |
|
2525
|
5103
|
|
|
|
|
8457
|
my $t = scalar @$trees; |
|
2526
|
5103
|
|
|
|
|
8591
|
my $c = $self->{c_psi}; |
|
2527
|
5103
|
|
|
|
|
10707
|
my ( $n_pts, $nf, $x_packed ) = $self->_resolve_input($data); |
|
2528
|
5103
|
|
|
|
|
9862
|
my $sums_packed = "\0" x ( $n_pts * 8 ); |
|
2529
|
|
|
|
|
|
|
score_all_xs( |
|
2530
|
|
|
|
|
|
|
$self->{_c_nodes}, $self->{_c_coef_idx}, $self->{_c_coef_val}, |
|
2531
|
|
|
|
|
|
|
$x_packed, $sums_packed, $n_pts, |
|
2532
|
|
|
|
|
|
|
$nf, $t, $self->{_use_openmp} |
|
2533
|
5103
|
|
|
|
|
246834
|
); |
|
2534
|
5103
|
|
|
|
|
10993
|
my $inv = log(2) / ( $c * $t ); |
|
2535
|
5103
|
|
|
|
|
9792
|
my $sum_threshold = -log($threshold) * $c * $t / log(2); |
|
2536
|
5103
|
|
|
|
|
8136
|
my $result = []; |
|
2537
|
5103
|
|
|
|
|
20630
|
score_predict_xs( $sums_packed, $n_pts, $inv, $sum_threshold, $result ); |
|
2538
|
5103
|
|
|
|
|
23586
|
return $result; |
|
2539
|
|
|
|
|
|
|
} ## end if ( $self->{_use_c} && $self->{_c_nodes} ...) |
|
2540
|
|
|
|
|
|
|
|
|
2541
|
|
|
|
|
|
|
# Fallback: edge thresholds, c==0, or no C backend. |
|
2542
|
2
|
|
|
|
|
10
|
my $scores = $self->score_samples( $self->_to_arrayref($data) ); |
|
2543
|
|
|
|
|
|
|
|
|
2544
|
2
|
|
|
|
|
7
|
my @to_return; |
|
2545
|
2
|
|
|
|
|
4
|
foreach my $score ( @{$scores} ) { |
|
|
2
|
|
|
|
|
5
|
|
|
2546
|
12
|
100
|
|
|
|
19
|
if ( $score >= $threshold ) { |
|
2547
|
4
|
|
|
|
|
10
|
push @to_return, [ $score, 1 ]; |
|
2548
|
|
|
|
|
|
|
} else { |
|
2549
|
8
|
|
|
|
|
16
|
push @to_return, [ $score, 0 ]; |
|
2550
|
|
|
|
|
|
|
} |
|
2551
|
|
|
|
|
|
|
} |
|
2552
|
|
|
|
|
|
|
|
|
2553
|
2
|
|
|
|
|
13
|
return \@to_return; |
|
2554
|
|
|
|
|
|
|
} ## end sub score_predict_samples |
|
2555
|
|
|
|
|
|
|
|
|
2556
|
|
|
|
|
|
|
=head2 score_predict_sample_tagged(\%row, $threshold) |
|
2557
|
|
|
|
|
|
|
|
|
2558
|
|
|
|
|
|
|
Scores and classifies a single sample supplied as a hashref of named |
|
2559
|
|
|
|
|
|
|
feature values. The model must have stored feature names. |
|
2560
|
|
|
|
|
|
|
|
|
2561
|
|
|
|
|
|
|
C<$threshold> defaults the same way as in C. |
|
2562
|
|
|
|
|
|
|
|
|
2563
|
|
|
|
|
|
|
Returns a two-element arrayref C<[$score, $label]>, matching the per-row |
|
2564
|
|
|
|
|
|
|
shape that C returns for each row. |
|
2565
|
|
|
|
|
|
|
|
|
2566
|
|
|
|
|
|
|
my $pair = $forest->score_predict_sample_tagged({ cpu => 0.9, mem => 0.4 }); |
|
2567
|
|
|
|
|
|
|
printf "score %.4f anomaly %d\n", $pair->[0], $pair->[1]; |
|
2568
|
|
|
|
|
|
|
|
|
2569
|
|
|
|
|
|
|
Croaks if the model has no stored feature names, if the hashref contains a |
|
2570
|
|
|
|
|
|
|
key that is not a known feature name, or if a feature name is absent from the |
|
2571
|
|
|
|
|
|
|
hashref. |
|
2572
|
|
|
|
|
|
|
|
|
2573
|
|
|
|
|
|
|
=cut |
|
2574
|
|
|
|
|
|
|
|
|
2575
|
|
|
|
|
|
|
sub score_predict_sample_tagged { |
|
2576
|
0
|
|
|
0
|
1
|
0
|
my ( $self, $row, $threshold ) = @_; |
|
2577
|
0
|
|
|
|
|
0
|
my $vec = $self->tagged_row_to_array( $row, 'score_predict_sample_tagged' ); |
|
2578
|
0
|
|
|
|
|
0
|
my $result = $self->score_predict_samples( [$vec], $threshold ); |
|
2579
|
0
|
|
|
|
|
0
|
return $result->[0]; |
|
2580
|
|
|
|
|
|
|
} |
|
2581
|
|
|
|
|
|
|
|
|
2582
|
|
|
|
|
|
|
=head2 score_predict_split(\@data, $threshold) |
|
2583
|
|
|
|
|
|
|
|
|
2584
|
|
|
|
|
|
|
Same data as L but returned as two flat arrayrefs |
|
2585
|
|
|
|
|
|
|
instead of an arrayref-of-pairs. Allocates roughly half as many Perl |
|
2586
|
|
|
|
|
|
|
SVs per point (no inner AV, no RV per row), so it is meaningfully faster |
|
2587
|
|
|
|
|
|
|
when both scores and labels are wanted but the paired shape is not. |
|
2588
|
|
|
|
|
|
|
|
|
2589
|
|
|
|
|
|
|
In list context returns C<($scores_aref, $labels_aref)>. |
|
2590
|
|
|
|
|
|
|
|
|
2591
|
|
|
|
|
|
|
my ($scores, $labels) = $forest->score_predict_split(\@data); |
|
2592
|
|
|
|
|
|
|
|
|
2593
|
|
|
|
|
|
|
for my $i (0 .. $#$scores) { |
|
2594
|
|
|
|
|
|
|
printf "%s -> score %.4f, label %d\n", |
|
2595
|
|
|
|
|
|
|
join(',', @{ $data[$i] }), $scores->[$i], $labels->[$i]; |
|
2596
|
|
|
|
|
|
|
} |
|
2597
|
|
|
|
|
|
|
|
|
2598
|
|
|
|
|
|
|
C<$threshold> defaults to the contamination-learned cutoff (if C |
|
2599
|
|
|
|
|
|
|
was called with C) or 0.5. |
|
2600
|
|
|
|
|
|
|
|
|
2601
|
|
|
|
|
|
|
=cut |
|
2602
|
|
|
|
|
|
|
|
|
2603
|
|
|
|
|
|
|
sub score_predict_split { |
|
2604
|
14114
|
|
|
14114
|
1
|
33772
|
my ( $self, $data, $threshold ) = @_; |
|
2605
|
|
|
|
|
|
|
$threshold |
|
2606
|
|
|
|
|
|
|
= defined $threshold ? $threshold |
|
2607
|
|
|
|
|
|
|
: defined $self->{threshold} ? $self->{threshold} |
|
2608
|
14114
|
50
|
|
|
|
26694
|
: 0.5; |
|
|
|
100
|
|
|
|
|
|
|
2609
|
14114
|
|
|
|
|
38414
|
$self->_check_fitted; |
|
2610
|
|
|
|
|
|
|
|
|
2611
|
|
|
|
|
|
|
# Fast path: fill two flat arrayrefs (scores + labels) directly from |
|
2612
|
|
|
|
|
|
|
# the sum buffer in one C call. Skips the inner AV + RV per point |
|
2613
|
|
|
|
|
|
|
# that score_predict_samples has to allocate. |
|
2614
|
14114
|
50
|
66
|
|
|
91916
|
if ( $self->{_use_c} |
|
|
|
|
33
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
2615
|
|
|
|
|
|
|
&& $self->{_c_nodes} |
|
2616
|
|
|
|
|
|
|
&& $self->{c_psi} > 0 |
|
2617
|
|
|
|
|
|
|
&& $threshold > 0 |
|
2618
|
|
|
|
|
|
|
&& $threshold < 1 ) |
|
2619
|
|
|
|
|
|
|
{ |
|
2620
|
14112
|
|
|
|
|
24722
|
my $trees = $self->{trees}; |
|
2621
|
14112
|
|
|
|
|
22413
|
my $t = scalar @$trees; |
|
2622
|
14112
|
|
|
|
|
23280
|
my $c = $self->{c_psi}; |
|
2623
|
14112
|
|
|
|
|
28182
|
my ( $n_pts, $nf, $x_packed ) = $self->_resolve_input($data); |
|
2624
|
14112
|
|
|
|
|
27310
|
my $sums_packed = "\0" x ( $n_pts * 8 ); |
|
2625
|
|
|
|
|
|
|
score_all_xs( |
|
2626
|
|
|
|
|
|
|
$self->{_c_nodes}, $self->{_c_coef_idx}, $self->{_c_coef_val}, |
|
2627
|
|
|
|
|
|
|
$x_packed, $sums_packed, $n_pts, |
|
2628
|
|
|
|
|
|
|
$nf, $t, $self->{_use_openmp} |
|
2629
|
14112
|
|
|
|
|
296150
|
); |
|
2630
|
14112
|
|
|
|
|
30949
|
my $inv = log(2) / ( $c * $t ); |
|
2631
|
14112
|
|
|
|
|
27148
|
my $sum_threshold = -log($threshold) * $c * $t / log(2); |
|
2632
|
14112
|
|
|
|
|
22452
|
my $scores = []; |
|
2633
|
14112
|
|
|
|
|
21229
|
my $labels = []; |
|
2634
|
14112
|
|
|
|
|
52047
|
score_predict_split_xs( $sums_packed, $n_pts, $inv, $sum_threshold, $scores, $labels ); |
|
2635
|
14112
|
|
|
|
|
69231
|
return ( $scores, $labels ); |
|
2636
|
|
|
|
|
|
|
} ## end if ( $self->{_use_c} && $self->{_c_nodes} ...) |
|
2637
|
|
|
|
|
|
|
|
|
2638
|
|
|
|
|
|
|
# Fallback: derive from score_samples. |
|
2639
|
2
|
|
|
|
|
9
|
my $scores = $self->score_samples( $self->_to_arrayref($data) ); |
|
2640
|
2
|
100
|
|
|
|
6
|
my @labels = map { $_ >= $threshold ? 1 : 0 } @$scores; |
|
|
12
|
|
|
|
|
26
|
|
|
2641
|
2
|
|
|
|
|
17
|
return ( $scores, \@labels ); |
|
2642
|
|
|
|
|
|
|
} ## end sub score_predict_split |
|
2643
|
|
|
|
|
|
|
|
|
2644
|
|
|
|
|
|
|
=head1 MODEL SAVE/LOAD METHODS |
|
2645
|
|
|
|
|
|
|
|
|
2646
|
|
|
|
|
|
|
=head2 to_json |
|
2647
|
|
|
|
|
|
|
|
|
2648
|
|
|
|
|
|
|
Returns a JSON representation of the model. |
|
2649
|
|
|
|
|
|
|
|
|
2650
|
|
|
|
|
|
|
Requires fit to have been called. |
|
2651
|
|
|
|
|
|
|
|
|
2652
|
|
|
|
|
|
|
my $json = $iforest->to_json; |
|
2653
|
|
|
|
|
|
|
|
|
2654
|
|
|
|
|
|
|
=cut |
|
2655
|
|
|
|
|
|
|
|
|
2656
|
|
|
|
|
|
|
sub to_json { |
|
2657
|
16
|
|
|
16
|
1
|
165923
|
my ($self) = @_; |
|
2658
|
16
|
|
|
|
|
99
|
$self->_check_fitted; |
|
2659
|
|
|
|
|
|
|
my $payload = { |
|
2660
|
|
|
|
|
|
|
format => 'Algorithm::Classifier::IsolationForest', |
|
2661
|
|
|
|
|
|
|
version => 1, |
|
2662
|
|
|
|
|
|
|
params => { |
|
2663
|
|
|
|
|
|
|
n_trees => $self->{n_trees}, |
|
2664
|
|
|
|
|
|
|
sample_size => $self->{sample_size}, |
|
2665
|
|
|
|
|
|
|
mode => $self->{mode}, |
|
2666
|
|
|
|
|
|
|
extension_level => $self->{extension_level_used}, |
|
2667
|
|
|
|
|
|
|
contamination => $self->{contamination}, |
|
2668
|
|
|
|
|
|
|
threshold => $self->{threshold}, |
|
2669
|
|
|
|
|
|
|
n_features => $self->{n_features}, |
|
2670
|
|
|
|
|
|
|
psi_used => $self->{psi_used}, |
|
2671
|
|
|
|
|
|
|
c_psi => $self->{c_psi}, |
|
2672
|
|
|
|
|
|
|
max_depth_used => $self->{max_depth_used}, |
|
2673
|
|
|
|
|
|
|
missing => $self->{missing}, |
|
2674
|
|
|
|
|
|
|
impute_with => $self->{impute_with}, |
|
2675
|
|
|
|
|
|
|
missing_fill => $self->{missing_fill}, |
|
2676
|
|
|
|
|
|
|
feature_names => $self->{feature_names}, |
|
2677
|
|
|
|
|
|
|
}, |
|
2678
|
|
|
|
|
|
|
trees => $self->{trees}, |
|
2679
|
14
|
|
|
|
|
479
|
}; |
|
2680
|
14
|
|
|
|
|
186
|
return JSON::PP->new->canonical(1)->encode($payload); |
|
2681
|
|
|
|
|
|
|
} ## end sub to_json |
|
2682
|
|
|
|
|
|
|
|
|
2683
|
|
|
|
|
|
|
=head2 from_json($json) |
|
2684
|
|
|
|
|
|
|
|
|
2685
|
|
|
|
|
|
|
Init the object from the model in the specified JSON string. |
|
2686
|
|
|
|
|
|
|
|
|
2687
|
|
|
|
|
|
|
my $iforest = Algorithm::Classifier::IsolationForest->from_json($json); |
|
2688
|
|
|
|
|
|
|
|
|
2689
|
|
|
|
|
|
|
=cut |
|
2690
|
|
|
|
|
|
|
|
|
2691
|
|
|
|
|
|
|
sub from_json { |
|
2692
|
22
|
|
|
22
|
1
|
3928761
|
my ( $class, $text ) = @_; |
|
2693
|
22
|
|
|
|
|
196
|
my $payload = JSON::PP->new->decode($text); |
|
2694
|
|
|
|
|
|
|
croak "not an IsolationForest model" |
|
2695
|
|
|
|
|
|
|
unless ref $payload eq 'HASH' |
|
2696
|
|
|
|
|
|
|
&& defined $payload->{format} |
|
2697
|
22
|
100
|
100
|
|
|
14848859
|
&& $payload->{format} eq 'Algorithm::Classifier::IsolationForest'; |
|
|
|
|
66
|
|
|
|
|
|
2698
|
|
|
|
|
|
|
|
|
2699
|
20
|
|
100
|
|
|
140
|
my $p = $payload->{params} || {}; |
|
2700
|
|
|
|
|
|
|
|
|
2701
|
|
|
|
|
|
|
# version 0 used hash-based nodes; version 1+ uses array-based nodes. |
|
2702
|
|
|
|
|
|
|
# Convert old models on load so the rest of the code only sees arrays. |
|
2703
|
20
|
|
50
|
|
|
179
|
my $trees = $payload->{trees} || []; |
|
2704
|
20
|
100
|
100
|
|
|
171
|
if ( ( $payload->{version} // 0 ) < 1 ) { |
|
2705
|
1
|
|
|
|
|
3
|
$trees = [ map { _hash_node_to_array($_) } @$trees ]; |
|
|
0
|
|
|
|
|
0
|
|
|
2706
|
|
|
|
|
|
|
} |
|
2707
|
|
|
|
|
|
|
|
|
2708
|
|
|
|
|
|
|
my $self = { |
|
2709
|
|
|
|
|
|
|
n_trees => $p->{n_trees}, |
|
2710
|
|
|
|
|
|
|
sample_size => $p->{sample_size}, |
|
2711
|
|
|
|
|
|
|
max_depth => undef, |
|
2712
|
|
|
|
|
|
|
seed => undef, |
|
2713
|
|
|
|
|
|
|
mode => $p->{mode} // 'axis', |
|
2714
|
|
|
|
|
|
|
extension_level => $p->{extension_level}, |
|
2715
|
|
|
|
|
|
|
extension_level_used => $p->{extension_level}, |
|
2716
|
|
|
|
|
|
|
contamination => $p->{contamination}, |
|
2717
|
|
|
|
|
|
|
threshold => $p->{threshold}, |
|
2718
|
|
|
|
|
|
|
n_features => $p->{n_features}, |
|
2719
|
|
|
|
|
|
|
psi_used => $p->{psi_used}, |
|
2720
|
|
|
|
|
|
|
c_psi => $p->{c_psi}, |
|
2721
|
|
|
|
|
|
|
max_depth_used => $p->{max_depth_used}, |
|
2722
|
|
|
|
|
|
|
# Models saved before missing-value support lack these keys; default |
|
2723
|
|
|
|
|
|
|
# to 'zero', which reproduces the old undef -> 0 scoring behaviour. |
|
2724
|
|
|
|
|
|
|
missing => $p->{missing} // 'zero', |
|
2725
|
|
|
|
|
|
|
impute_with => $p->{impute_with} // 'mean', |
|
2726
|
|
|
|
|
|
|
missing_fill => $p->{missing_fill}, |
|
2727
|
|
|
|
|
|
|
feature_names => $p->{feature_names}, |
|
2728
|
20
|
|
100
|
|
|
630
|
trees => $trees, |
|
|
|
|
100
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
2729
|
|
|
|
|
|
|
_use_c => $HAS_C, |
|
2730
|
|
|
|
|
|
|
_use_openmp => $HAS_OPENMP, |
|
2731
|
|
|
|
|
|
|
_use_openmp_fit => 0, # opt-in only; loaded models never re-fit implicitly |
|
2732
|
|
|
|
|
|
|
}; |
|
2733
|
20
|
100
|
|
|
|
101
|
croak "model contains no trees" unless @{ $self->{trees} }; |
|
|
20
|
|
|
|
|
216
|
|
|
2734
|
|
|
|
|
|
|
|
|
2735
|
|
|
|
|
|
|
# Recompute the normalising constant from the (integer, exact) sub-sample |
|
2736
|
|
|
|
|
|
|
# size rather than trusting the stored float, so a reloaded model's scores |
|
2737
|
|
|
|
|
|
|
# are bit-for-bit identical to the original's. |
|
2738
|
19
|
50
|
|
|
|
191
|
$self->{c_psi} = _c( $self->{psi_used} ) if defined $self->{psi_used}; |
|
2739
|
|
|
|
|
|
|
|
|
2740
|
19
|
|
|
|
|
91
|
my $model = bless $self, $class; |
|
2741
|
19
|
50
|
|
|
|
278
|
$model->_rebuild_c_trees() if $self->{_use_c}; |
|
2742
|
19
|
|
|
|
|
283
|
return $model; |
|
2743
|
|
|
|
|
|
|
} ## end sub from_json |
|
2744
|
|
|
|
|
|
|
|
|
2745
|
|
|
|
|
|
|
=head2 save($path) |
|
2746
|
|
|
|
|
|
|
|
|
2747
|
|
|
|
|
|
|
Saves the model to the specified path. |
|
2748
|
|
|
|
|
|
|
|
|
2749
|
|
|
|
|
|
|
$iforest->save($path); |
|
2750
|
|
|
|
|
|
|
|
|
2751
|
|
|
|
|
|
|
=cut |
|
2752
|
|
|
|
|
|
|
|
|
2753
|
|
|
|
|
|
|
sub save { |
|
2754
|
1
|
|
|
1
|
1
|
4689
|
my ( $self, $path ) = @_; |
|
2755
|
1
|
|
|
|
|
6
|
write_file( $path, { 'atomic' => 1 }, $self->to_json ); |
|
2756
|
|
|
|
|
|
|
} |
|
2757
|
|
|
|
|
|
|
|
|
2758
|
|
|
|
|
|
|
=head2 load($path) |
|
2759
|
|
|
|
|
|
|
|
|
2760
|
|
|
|
|
|
|
Init the object from the model in the specified file. |
|
2761
|
|
|
|
|
|
|
|
|
2762
|
|
|
|
|
|
|
my $iforest = Algorithm::Classifier::IsolationForest->load($path); |
|
2763
|
|
|
|
|
|
|
|
|
2764
|
|
|
|
|
|
|
=cut |
|
2765
|
|
|
|
|
|
|
|
|
2766
|
|
|
|
|
|
|
sub load { |
|
2767
|
9
|
|
|
9
|
1
|
155067
|
my ( $class, $path ) = @_; |
|
2768
|
9
|
|
|
|
|
60
|
my $raw_model = read_file($path); |
|
2769
|
8
|
|
|
|
|
986
|
return $class->from_json($raw_model); |
|
2770
|
|
|
|
|
|
|
} |
|
2771
|
|
|
|
|
|
|
|
|
2772
|
|
|
|
|
|
|
=head1 REFERENCES |
|
2773
|
|
|
|
|
|
|
|
|
2774
|
|
|
|
|
|
|
Liu, Fei Tony & Ting, Kai & Zhou, Zhi-Hua. (2008). Isolation Forest. 413 - 422. 10.1109/ICDM.2008.17. |
|
2775
|
|
|
|
|
|
|
|
|
2776
|
|
|
|
|
|
|
L |
|
2777
|
|
|
|
|
|
|
|
|
2778
|
|
|
|
|
|
|
L |
|
2779
|
|
|
|
|
|
|
|
|
2780
|
|
|
|
|
|
|
Sahand Hariri, Matias Carrasco Kind, Robert J. Brunner (2020). Extended Isolation Forest. 1479 - 1489. 10.1109/TKDE.2019.2947676 |
|
2781
|
|
|
|
|
|
|
|
|
2782
|
|
|
|
|
|
|
L |
|
2783
|
|
|
|
|
|
|
|
|
2784
|
|
|
|
|
|
|
=cut |
|
2785
|
|
|
|
|
|
|
|
|
2786
|
|
|
|
|
|
|
### |
|
2787
|
|
|
|
|
|
|
### |
|
2788
|
|
|
|
|
|
|
### internal stuff below |
|
2789
|
|
|
|
|
|
|
### |
|
2790
|
|
|
|
|
|
|
### |
|
2791
|
|
|
|
|
|
|
|
|
2792
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
2793
|
|
|
|
|
|
|
# c(n): the expected path length of an unsuccessful search in a binary search |
|
2794
|
|
|
|
|
|
|
# tree of n nodes. Isolation Forest uses it (a) to adjust the path length when a |
|
2795
|
|
|
|
|
|
|
# leaf still holds more than one point (depth limit reached), and (b) to |
|
2796
|
|
|
|
|
|
|
# normalise the average path length into a 0..1 anomaly score. |
|
2797
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
2798
|
|
|
|
|
|
|
sub _c { |
|
2799
|
581580
|
|
|
581580
|
|
696144
|
my ($n) = @_; |
|
2800
|
581580
|
100
|
|
|
|
983409
|
return 0.0 if $n <= 1; |
|
2801
|
339714
|
100
|
|
|
|
498962
|
return 1.0 if $n == 2; |
|
2802
|
281436
|
|
|
|
|
360568
|
my $harmonic = log( $n - 1 ) + EULER; # H(n-1) ~= ln(n-1) + gamma |
|
2803
|
281436
|
|
|
|
|
527748
|
return 2.0 * $harmonic - ( 2.0 * ( $n - 1 ) / $n ); |
|
2804
|
|
|
|
|
|
|
} |
|
2805
|
|
|
|
|
|
|
|
|
2806
|
|
|
|
|
|
|
# One draw from the standard normal N(0,1) via Box-Muller. Used to pick the |
|
2807
|
|
|
|
|
|
|
# random hyperplane orientations in Extended Isolation Forest mode. |
|
2808
|
|
|
|
|
|
|
sub _randn { |
|
2809
|
42769
|
|
50
|
42769
|
|
61185
|
my $u1 = rand() || 1e-12; |
|
2810
|
42769
|
|
|
|
|
47550
|
my $u2 = rand(); |
|
2811
|
42769
|
|
|
|
|
66943
|
return sqrt( -2.0 * log($u1) ) * cos( TWO_PI * $u2 ); |
|
2812
|
|
|
|
|
|
|
} |
|
2813
|
|
|
|
|
|
|
|
|
2814
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
2815
|
|
|
|
|
|
|
# Draw $k samples without replacement via a partial Fisher-Yates shuffle of the |
|
2816
|
|
|
|
|
|
|
# index array. Returns an arrayref of (shared, read-only) sample refs. |
|
2817
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
2818
|
|
|
|
|
|
|
sub _subsample { |
|
2819
|
2169
|
|
|
2169
|
|
4678
|
my ( $data, $k ) = @_; |
|
2820
|
2169
|
|
|
|
|
3256
|
my $n = scalar @$data; |
|
2821
|
2169
|
|
|
|
|
14785
|
my @idx = ( 0 .. $n - 1 ); |
|
2822
|
2169
|
|
|
|
|
5256
|
for my $i ( 0 .. $k - 1 ) { |
|
2823
|
392648
|
|
|
|
|
455306
|
my $j = $i + int( rand( $n - $i ) ); |
|
2824
|
392648
|
|
|
|
|
516751
|
@idx[ $i, $j ] = @idx[ $j, $i ]; |
|
2825
|
|
|
|
|
|
|
} |
|
2826
|
2169
|
|
|
|
|
30071
|
my @chosen = @idx[ 0 .. $k - 1 ]; |
|
2827
|
2169
|
|
|
|
|
9897
|
return [ @{$data}[@chosen] ]; |
|
|
2169
|
|
|
|
|
42942
|
|
|
2828
|
|
|
|
|
|
|
} ## end sub _subsample |
|
2829
|
|
|
|
|
|
|
|
|
2830
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
2831
|
|
|
|
|
|
|
# Recursively build one isolation tree. |
|
2832
|
|
|
|
|
|
|
# |
|
2833
|
|
|
|
|
|
|
# A node is one of: |
|
2834
|
|
|
|
|
|
|
# leaf { leaf => 1, size => N } |
|
2835
|
|
|
|
|
|
|
# axis { attr => A, split => S, left => ..., right => ... } |
|
2836
|
|
|
|
|
|
|
# oblique { idx => [..], coef => [..], b => B, left => ..., right => ... } |
|
2837
|
|
|
|
|
|
|
# |
|
2838
|
|
|
|
|
|
|
# In both split styles the choice is restricted to features that actually vary |
|
2839
|
|
|
|
|
|
|
# across the points reaching the node: this avoids wasted levels on constant |
|
2840
|
|
|
|
|
|
|
# columns and lets a node leaf out exactly when its points are indistinguishable. |
|
2841
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
2842
|
|
|
|
|
|
|
sub _build_tree { |
|
2843
|
293547
|
|
|
293547
|
|
395236
|
my ( $self, $X, $depth, $limit ) = @_; |
|
2844
|
|
|
|
|
|
|
|
|
2845
|
293547
|
|
|
|
|
327231
|
my $size = scalar @$X; |
|
2846
|
293547
|
100
|
100
|
|
|
680064
|
return [ _NODE_LEAF, $size ] |
|
2847
|
|
|
|
|
|
|
if $depth >= $limit || $size <= 1; |
|
2848
|
|
|
|
|
|
|
|
|
2849
|
148484
|
|
|
|
|
191873
|
my $nf = $self->{n_features}; |
|
2850
|
148484
|
|
|
|
|
191479
|
my $nan = $self->{missing} eq 'nan'; |
|
2851
|
|
|
|
|
|
|
|
|
2852
|
|
|
|
|
|
|
# Per-feature min and max within this node, in a single pass. Missing |
|
2853
|
|
|
|
|
|
|
# (undef) cells never reach here under die/zero/impute -- those fill the |
|
2854
|
|
|
|
|
|
|
# data before fit -- so the "next unless defined" guard is only needed |
|
2855
|
|
|
|
|
|
|
# in nan mode, where missing values must not constrain a feature's |
|
2856
|
|
|
|
|
|
|
# range; every other strategy skips it since every cell is defined and |
|
2857
|
|
|
|
|
|
|
# the check would never fire. |
|
2858
|
148484
|
|
|
|
|
168514
|
my ( @lo, @hi ); |
|
2859
|
148484
|
100
|
|
|
|
185644
|
if ($nan) { |
|
2860
|
23338
|
|
|
|
|
28631
|
for my $row (@$X) { |
|
2861
|
383160
|
|
|
|
|
478517
|
for my $f ( 0 .. $nf - 1 ) { |
|
2862
|
770777
|
|
|
|
|
843652
|
my $v = $row->[$f]; |
|
2863
|
770777
|
100
|
|
|
|
1005991
|
next unless defined $v; |
|
2864
|
698528
|
100
|
100
|
|
|
1362097
|
$lo[$f] = $v if !defined $lo[$f] || $v < $lo[$f]; |
|
2865
|
698528
|
100
|
100
|
|
|
1512791
|
$hi[$f] = $v if !defined $hi[$f] || $v > $hi[$f]; |
|
2866
|
|
|
|
|
|
|
} |
|
2867
|
|
|
|
|
|
|
} |
|
2868
|
|
|
|
|
|
|
} else { |
|
2869
|
125146
|
|
|
|
|
156097
|
for my $row (@$X) { |
|
2870
|
2588791
|
|
|
|
|
3201420
|
for my $f ( 0 .. $nf - 1 ) { |
|
2871
|
7021721
|
|
|
|
|
7665550
|
my $v = $row->[$f]; |
|
2872
|
7021721
|
100
|
100
|
|
|
13646042
|
$lo[$f] = $v if !defined $lo[$f] || $v < $lo[$f]; |
|
2873
|
7021721
|
100
|
100
|
|
|
14872432
|
$hi[$f] = $v if !defined $hi[$f] || $v > $hi[$f]; |
|
2874
|
|
|
|
|
|
|
} |
|
2875
|
|
|
|
|
|
|
} |
|
2876
|
|
|
|
|
|
|
} |
|
2877
|
|
|
|
|
|
|
|
|
2878
|
|
|
|
|
|
|
# Features with spread are the only ones that can split the data. A |
|
2879
|
|
|
|
|
|
|
# feature whose values are all missing within this node has an undef |
|
2880
|
|
|
|
|
|
|
# range and is excluded. |
|
2881
|
148484
|
100
|
|
|
|
231864
|
my @varying = grep { defined $lo[$_] && $lo[$_] < $hi[$_] } 0 .. $nf - 1; |
|
|
331276
|
|
|
|
|
766894
|
|
|
2882
|
|
|
|
|
|
|
|
|
2883
|
|
|
|
|
|
|
# No spread on any feature => all points identical => cannot isolate. |
|
2884
|
148484
|
100
|
|
|
|
220385
|
return [ _NODE_LEAF, $size ] unless @varying; |
|
2885
|
|
|
|
|
|
|
|
|
2886
|
|
|
|
|
|
|
my $node |
|
2887
|
145689
|
100
|
|
|
|
282919
|
= $self->{mode} eq 'extended' |
|
2888
|
|
|
|
|
|
|
? $self->_oblique_split( $X, \@varying, \@lo, \@hi, $nan ) |
|
2889
|
|
|
|
|
|
|
: _axis_split( $X, \@varying, \@lo, \@hi, $nan ); |
|
2890
|
|
|
|
|
|
|
|
|
2891
|
|
|
|
|
|
|
# Split functions leave the raw point arrays at the child slots so that |
|
2892
|
|
|
|
|
|
|
# _build_tree can recurse into them; the subtree refs replace them in-place. |
|
2893
|
|
|
|
|
|
|
# Axis nodes: left at [3], right at [4] |
|
2894
|
|
|
|
|
|
|
# Oblique nodes: left at [4], right at [5] |
|
2895
|
145689
|
100
|
|
|
|
239628
|
my ( $li, $ri ) = $node->[0] == _NODE_AXIS ? ( 3, 4 ) : ( 4, 5 ); |
|
2896
|
145689
|
|
|
|
|
242547
|
$node->[$li] = $self->_build_tree( $node->[$li], $depth + 1, $limit ); |
|
2897
|
145689
|
|
|
|
|
214253
|
$node->[$ri] = $self->_build_tree( $node->[$ri], $depth + 1, $limit ); |
|
2898
|
|
|
|
|
|
|
|
|
2899
|
145689
|
|
|
|
|
337096
|
return $node; |
|
2900
|
|
|
|
|
|
|
} ## end sub _build_tree |
|
2901
|
|
|
|
|
|
|
|
|
2902
|
|
|
|
|
|
|
# Axis-parallel cut: random varying feature, random threshold in its range. |
|
2903
|
|
|
|
|
|
|
# Returns [_NODE_AXIS, attr, split, \@left_pts, \@right_pts]. |
|
2904
|
|
|
|
|
|
|
# _build_tree overwrites slots 3 and 4 with the recursed subtrees. |
|
2905
|
|
|
|
|
|
|
sub _axis_split { |
|
2906
|
123302
|
|
|
123302
|
|
180739
|
my ( $X, $varying, $lo, $hi, $nan ) = @_; |
|
2907
|
|
|
|
|
|
|
|
|
2908
|
123302
|
|
|
|
|
193852
|
my $attr = $varying->[ int( rand( scalar @$varying ) ) ]; |
|
2909
|
123302
|
|
|
|
|
174578
|
my $split = $lo->[$attr] + rand() * ( $hi->[$attr] - $lo->[$attr] ); |
|
2910
|
|
|
|
|
|
|
|
|
2911
|
|
|
|
|
|
|
# A point missing the split feature (nan mode only) routes to the right |
|
2912
|
|
|
|
|
|
|
# child -- the same side NaN reaches in the C scorer, where (NaN < split) |
|
2913
|
|
|
|
|
|
|
# is false. Under die/zero/impute every cell is defined, so the |
|
2914
|
|
|
|
|
|
|
# "defined($v)" guard is dead weight there and skipped entirely. |
|
2915
|
123302
|
|
|
|
|
141042
|
my ( @left, @right ); |
|
2916
|
123302
|
100
|
|
|
|
150403
|
if ($nan) { |
|
2917
|
15096
|
|
|
|
|
18552
|
for my $row (@$X) { |
|
2918
|
239367
|
|
|
|
|
256055
|
my $v = $row->[$attr]; |
|
2919
|
239367
|
100
|
100
|
|
|
427541
|
if ( defined($v) && $v < $split ) { push @left, $row } |
|
|
111298
|
|
|
|
|
152124
|
|
|
2920
|
128069
|
|
|
|
|
177176
|
else { push @right, $row } |
|
2921
|
|
|
|
|
|
|
} |
|
2922
|
|
|
|
|
|
|
} else { |
|
2923
|
108206
|
|
|
|
|
137866
|
for my $row (@$X) { |
|
2924
|
2189925
|
100
|
|
|
|
2675247
|
if ( $row->[$attr] < $split ) { push @left, $row } |
|
|
1093141
|
|
|
|
|
1401645
|
|
|
2925
|
1096784
|
|
|
|
|
1383669
|
else { push @right, $row } |
|
2926
|
|
|
|
|
|
|
} |
|
2927
|
|
|
|
|
|
|
} |
|
2928
|
123302
|
|
|
|
|
320191
|
return [ _NODE_AXIS, $attr, $split, \@left, \@right ]; |
|
2929
|
|
|
|
|
|
|
} ## end sub _axis_split |
|
2930
|
|
|
|
|
|
|
|
|
2931
|
|
|
|
|
|
|
# Oblique cut (Extended Isolation Forest): a random hyperplane. We activate |
|
2932
|
|
|
|
|
|
|
# (extension_level + 1) of the varying features, give each a Gaussian |
|
2933
|
|
|
|
|
|
|
# coefficient, and place the plane through a random point in the bounding box. |
|
2934
|
|
|
|
|
|
|
# A point goes left when coef . x <= b, where b = coef . p. |
|
2935
|
|
|
|
|
|
|
# Returns [_NODE_OBLIQUE, \@idx, \@coef, $b, \@left_pts, \@right_pts]. |
|
2936
|
|
|
|
|
|
|
# _build_tree overwrites slots 4 and 5 with the recursed subtrees. |
|
2937
|
|
|
|
|
|
|
sub _oblique_split { |
|
2938
|
22387
|
|
|
22387
|
|
33787
|
my ( $self, $X, $varying, $lo, $hi, $nan ) = @_; |
|
2939
|
|
|
|
|
|
|
|
|
2940
|
22387
|
|
|
|
|
28834
|
my $active = $self->{extension_level_used} + 1; |
|
2941
|
22387
|
100
|
|
|
|
34338
|
$active = scalar @$varying if $active > scalar @$varying; |
|
2942
|
|
|
|
|
|
|
|
|
2943
|
|
|
|
|
|
|
# Pick which varying features take part (partial shuffle of their indices). |
|
2944
|
22387
|
|
|
|
|
31771
|
my @pool = @$varying; |
|
2945
|
22387
|
|
|
|
|
31822
|
for my $i ( 0 .. $active - 1 ) { |
|
2946
|
42769
|
|
|
|
|
58241
|
my $j = $i + int( rand( scalar(@pool) - $i ) ); |
|
2947
|
42769
|
|
|
|
|
63594
|
@pool[ $i, $j ] = @pool[ $j, $i ]; |
|
2948
|
|
|
|
|
|
|
} |
|
2949
|
22387
|
|
|
|
|
38959
|
my @idx = @pool[ 0 .. $active - 1 ]; |
|
2950
|
|
|
|
|
|
|
|
|
2951
|
22387
|
|
|
|
|
27487
|
my ( @coef, $b ); |
|
2952
|
22387
|
|
|
|
|
25216
|
$b = 0.0; |
|
2953
|
22387
|
|
|
|
|
27960
|
for my $f (@idx) { |
|
2954
|
42769
|
|
|
|
|
52243
|
my $c = _randn(); |
|
2955
|
42769
|
|
|
|
|
59383
|
my $p = $lo->[$f] + rand() * ( $hi->[$f] - $lo->[$f] ); # point in the box |
|
2956
|
42769
|
|
|
|
|
53833
|
push @coef, $c; |
|
2957
|
42769
|
|
|
|
|
53561
|
$b += $c * $p; |
|
2958
|
|
|
|
|
|
|
} |
|
2959
|
|
|
|
|
|
|
|
|
2960
|
|
|
|
|
|
|
# A point missing any feature on the hyperplane (nan mode only) routes |
|
2961
|
|
|
|
|
|
|
# to the right child: in the C scorer the dot product becomes NaN and |
|
2962
|
|
|
|
|
|
|
# (NaN <= b) is false, so this keeps fit and score consistent. Under |
|
2963
|
|
|
|
|
|
|
# die/zero/impute every cell is defined, so the per-feature "defined" |
|
2964
|
|
|
|
|
|
|
# check and early-exit are dead weight there and skipped entirely. |
|
2965
|
22387
|
|
|
|
|
26945
|
my ( @left, @right ); |
|
2966
|
22387
|
100
|
|
|
|
29591
|
if ($nan) { |
|
2967
|
6947
|
|
|
|
|
8574
|
for my $row (@$X) { |
|
2968
|
139686
|
|
|
|
|
146955
|
my $dot = 0.0; |
|
2969
|
139686
|
|
|
|
|
143923
|
my $missing = 0; |
|
2970
|
139686
|
|
|
|
|
195400
|
for ( 0 .. $#idx ) { |
|
2971
|
262282
|
|
|
|
|
294225
|
my $v = $row->[ $idx[$_] ]; |
|
2972
|
262282
|
100
|
|
|
|
328324
|
if ( !defined $v ) { $missing = 1; last } |
|
|
26240
|
|
|
|
|
27281
|
|
|
|
26240
|
|
|
|
|
27964
|
|
|
2973
|
236042
|
|
|
|
|
293488
|
$dot += $coef[$_] * $v; |
|
2974
|
|
|
|
|
|
|
} |
|
2975
|
139686
|
100
|
100
|
|
|
254368
|
if ( !$missing && $dot <= $b ) { push @left, $row } |
|
|
57761
|
|
|
|
|
77510
|
|
|
2976
|
81925
|
|
|
|
|
114468
|
else { push @right, $row } |
|
2977
|
|
|
|
|
|
|
} ## end for my $row (@$X) |
|
2978
|
|
|
|
|
|
|
} else { |
|
2979
|
15440
|
|
|
|
|
18913
|
for my $row (@$X) { |
|
2980
|
394204
|
|
|
|
|
404669
|
my $dot = 0.0; |
|
2981
|
394204
|
|
|
|
|
690880
|
$dot += $coef[$_] * $row->[ $idx[$_] ] for 0 .. $#idx; |
|
2982
|
394204
|
100
|
|
|
|
480426
|
if ( $dot <= $b ) { push @left, $row } |
|
|
193814
|
|
|
|
|
240824
|
|
|
2983
|
200390
|
|
|
|
|
248902
|
else { push @right, $row } |
|
2984
|
|
|
|
|
|
|
} |
|
2985
|
|
|
|
|
|
|
} |
|
2986
|
22387
|
|
|
|
|
73709
|
return [ _NODE_OBLIQUE, \@idx, \@coef, $b, \@left, \@right ]; |
|
2987
|
|
|
|
|
|
|
} ## end sub _oblique_split |
|
2988
|
|
|
|
|
|
|
|
|
2989
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
2990
|
|
|
|
|
|
|
# Path length of a single point in a single tree: edges traversed until a leaf, |
|
2991
|
|
|
|
|
|
|
# plus c(leaf size) when the leaf still holds several points. |
|
2992
|
|
|
|
|
|
|
# |
|
2993
|
|
|
|
|
|
|
# Node layout (arrayref, slot 0 = type): |
|
2994
|
|
|
|
|
|
|
# _NODE_LEAF [0, size] |
|
2995
|
|
|
|
|
|
|
# _NODE_AXIS [1, attr, split, left, right] |
|
2996
|
|
|
|
|
|
|
# _NODE_OBLIQUE [2, \@idx, \@coef, b, left, right] |
|
2997
|
|
|
|
|
|
|
# |
|
2998
|
|
|
|
|
|
|
# The type tag is also used as a loop sentinel: 0 (_NODE_LEAF) is falsy. |
|
2999
|
|
|
|
|
|
|
# No $self argument -- the node type encodes everything needed. |
|
3000
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
3001
|
|
|
|
|
|
|
# The optional $nan flag selects the nan-strategy routing: a point missing |
|
3002
|
|
|
|
|
|
|
# the split feature goes to the right child (matching the C scorer, where |
|
3003
|
|
|
|
|
|
|
# the NaN comparison is false). Without it, undef is coerced to 0 -- the |
|
3004
|
|
|
|
|
|
|
# behaviour the die/zero/impute strategies rely on (their data is dense by |
|
3005
|
|
|
|
|
|
|
# the time it reaches here, so the "// 0" is normally a no-op). |
|
3006
|
|
|
|
|
|
|
sub _path_length { |
|
3007
|
340276
|
|
|
340276
|
|
443564
|
my ( $x, $node, $depth, $nan ) = @_; |
|
3008
|
340276
|
|
|
|
|
459510
|
while ( $node->[0] ) { # false only for leaf (type 0) |
|
3009
|
2481278
|
100
|
|
|
|
2989003
|
if ( $node->[0] == _NODE_AXIS ) { # [1, attr, split, left, right] |
|
3010
|
2283722
|
100
|
|
|
|
2668214
|
if ($nan) { |
|
3011
|
4852
|
|
|
|
|
5327
|
my $v = $x->[ $node->[1] ]; |
|
3012
|
4852
|
100
|
100
|
|
|
9421
|
$node = ( defined($v) && $v < $node->[2] ) ? $node->[3] : $node->[4]; |
|
3013
|
|
|
|
|
|
|
} else { |
|
3014
|
2278870
|
100
|
100
|
|
|
3735941
|
$node = ( $x->[ $node->[1] ] // 0 ) < $node->[2] ? $node->[3] : $node->[4]; |
|
3015
|
|
|
|
|
|
|
} |
|
3016
|
|
|
|
|
|
|
} else { # [2, \@idx, \@coef, b, left, right] |
|
3017
|
197556
|
|
|
|
|
255472
|
my ( $idx, $coef, $b ) = ( $node->[1], $node->[2], $node->[3] ); |
|
3018
|
197556
|
100
|
|
|
|
228010
|
if ($nan) { |
|
3019
|
3034
|
|
|
|
|
3146
|
my $dot = 0.0; |
|
3020
|
3034
|
|
|
|
|
3194
|
my $missing = 0; |
|
3021
|
3034
|
|
|
|
|
4061
|
for ( 0 .. $#$idx ) { |
|
3022
|
4727
|
|
|
|
|
6023
|
my $v = $x->[ $idx->[$_] ]; |
|
3023
|
4727
|
100
|
|
|
|
5968
|
if ( !defined $v ) { $missing = 1; last } |
|
|
1920
|
|
|
|
|
1988
|
|
|
|
1920
|
|
|
|
|
2068
|
|
|
3024
|
2807
|
|
|
|
|
3754
|
$dot += $coef->[$_] * $v; |
|
3025
|
|
|
|
|
|
|
} |
|
3026
|
3034
|
100
|
100
|
|
|
5627
|
$node = ( !$missing && $dot <= $b ) ? $node->[4] : $node->[5]; |
|
3027
|
|
|
|
|
|
|
} else { |
|
3028
|
194522
|
|
|
|
|
196625
|
my $dot = 0.0; |
|
3029
|
194522
|
|
100
|
|
|
445154
|
$dot += $coef->[$_] * ( $x->[ $idx->[$_] ] // 0 ) for 0 .. $#$idx; |
|
3030
|
194522
|
100
|
|
|
|
268591
|
$node = $dot <= $b ? $node->[4] : $node->[5]; |
|
3031
|
|
|
|
|
|
|
} |
|
3032
|
|
|
|
|
|
|
} ## end else [ if ( $node->[0] == _NODE_AXIS ) ] |
|
3033
|
2481278
|
|
|
|
|
3339127
|
$depth++; |
|
3034
|
|
|
|
|
|
|
} ## end while ( $node->[0] ) |
|
3035
|
340276
|
|
|
|
|
430786
|
return $depth + _c( $node->[1] ); # leaf size at slot 1 |
|
3036
|
|
|
|
|
|
|
} ## end sub _path_length |
|
3037
|
|
|
|
|
|
|
|
|
3038
|
|
|
|
|
|
|
# Recursively convert a version-0 hash-based tree node to the version-1 |
|
3039
|
|
|
|
|
|
|
# array format. Called by from_json when loading an old saved model. |
|
3040
|
|
|
|
|
|
|
sub _hash_node_to_array { |
|
3041
|
0
|
|
|
0
|
|
0
|
my ($node) = @_; |
|
3042
|
0
|
0
|
|
|
|
0
|
if ( $node->{leaf} ) { |
|
|
|
0
|
|
|
|
|
|
|
3043
|
0
|
|
|
|
|
0
|
return [ _NODE_LEAF, $node->{size} ]; |
|
3044
|
|
|
|
|
|
|
} elsif ( exists $node->{attr} ) { |
|
3045
|
|
|
|
|
|
|
return [ |
|
3046
|
|
|
|
|
|
|
_NODE_AXIS, $node->{attr}, |
|
3047
|
|
|
|
|
|
|
$node->{split}, _hash_node_to_array( $node->{left} ), |
|
3048
|
0
|
|
|
|
|
0
|
_hash_node_to_array( $node->{right} ), |
|
3049
|
|
|
|
|
|
|
]; |
|
3050
|
|
|
|
|
|
|
} else { |
|
3051
|
|
|
|
|
|
|
return [ |
|
3052
|
|
|
|
|
|
|
_NODE_OBLIQUE, $node->{idx}, $node->{coef}, $node->{b}, |
|
3053
|
|
|
|
|
|
|
_hash_node_to_array( $node->{left} ), |
|
3054
|
0
|
|
|
|
|
0
|
_hash_node_to_array( $node->{right} ), |
|
3055
|
|
|
|
|
|
|
]; |
|
3056
|
|
|
|
|
|
|
} |
|
3057
|
|
|
|
|
|
|
} ## end sub _hash_node_to_array |
|
3058
|
|
|
|
|
|
|
|
|
3059
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
3060
|
|
|
|
|
|
|
# _pack_tree($root) -- flatten one tree into three packed buffers. |
|
3061
|
|
|
|
|
|
|
# |
|
3062
|
|
|
|
|
|
|
# Returns ($nodes_packed, $idx_packed, $val_packed) where: |
|
3063
|
|
|
|
|
|
|
# nodes_packed: 6 doubles per node (see score_all_xs comment above) |
|
3064
|
|
|
|
|
|
|
# idx_packed: int32 feature indices for every oblique-node coefficient |
|
3065
|
|
|
|
|
|
|
# val_packed: double values matching idx_packed one-for-one |
|
3066
|
|
|
|
|
|
|
# |
|
3067
|
|
|
|
|
|
|
# Storing idx and val in separate buffers (SoA) instead of interleaved |
|
3068
|
|
|
|
|
|
|
# doubles lets the oblique dot product's SIMD inner loop run over a |
|
3069
|
|
|
|
|
|
|
# contiguous val[] stream without a per-iteration (int) cast, and |
|
3070
|
|
|
|
|
|
|
# halves the index bandwidth (int32 vs double). The same `coff` |
|
3071
|
|
|
|
|
|
|
# offset addresses paired entries in both buffers. |
|
3072
|
|
|
|
|
|
|
# |
|
3073
|
|
|
|
|
|
|
# Nodes are numbered in DFS pre-order: the root is always index 0 and |
|
3074
|
|
|
|
|
|
|
# children always get indices larger than their parent's. |
|
3075
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
3076
|
|
|
|
|
|
|
sub _pack_tree { |
|
3077
|
4092
|
|
|
4092
|
|
6226
|
my ( $root, $n_features ) = @_; |
|
3078
|
4092
|
|
|
|
|
7511
|
my ( @node_data, @coef_idx, @coef_val ); |
|
3079
|
|
|
|
|
|
|
|
|
3080
|
4092
|
|
|
|
|
0
|
my $assign; |
|
3081
|
|
|
|
|
|
|
$assign = sub { |
|
3082
|
478238
|
|
|
478238
|
|
567194
|
my ($node) = @_; |
|
3083
|
478238
|
|
|
|
|
513545
|
my $my_idx = scalar @node_data; |
|
3084
|
478238
|
|
|
|
|
570721
|
push @node_data, undef; # reserve slot; filled in after children |
|
3085
|
|
|
|
|
|
|
|
|
3086
|
478238
|
100
|
|
|
|
728606
|
if ( $node->[0] == _NODE_LEAF ) { |
|
|
|
100
|
|
|
|
|
|
|
3087
|
|
|
|
|
|
|
|
|
3088
|
|
|
|
|
|
|
# Slot 2 carries c(size) precomputed, so the C scoring loop |
|
3089
|
|
|
|
|
|
|
# adds it straight to the depth instead of paying a log() |
|
3090
|
|
|
|
|
|
|
# per point per tree at every leaf hit. _c is the same |
|
3091
|
|
|
|
|
|
|
# function the pure-Perl scorer uses, so both backends keep |
|
3092
|
|
|
|
|
|
|
# producing bit-identical path lengths. |
|
3093
|
241165
|
|
|
|
|
316834
|
$node_data[$my_idx] = [ 0.0, $node->[1] + 0.0, _c( $node->[1] ), 0.0, 0.0, 0.0 ]; |
|
3094
|
|
|
|
|
|
|
} elsif ( $node->[0] == _NODE_AXIS ) { |
|
3095
|
208282
|
|
|
|
|
322333
|
my $li = $assign->( $node->[3] ); |
|
3096
|
208282
|
|
|
|
|
264123
|
my $ri = $assign->( $node->[4] ); |
|
3097
|
208282
|
|
|
|
|
398655
|
$node_data[$my_idx] = [ |
|
3098
|
|
|
|
|
|
|
1.0, |
|
3099
|
|
|
|
|
|
|
$node->[1] + 0.0, # attr |
|
3100
|
|
|
|
|
|
|
$node->[2] + 0.0, # split |
|
3101
|
|
|
|
|
|
|
$li + 0.0, |
|
3102
|
|
|
|
|
|
|
$ri + 0.0, |
|
3103
|
|
|
|
|
|
|
0.0, |
|
3104
|
|
|
|
|
|
|
]; |
|
3105
|
|
|
|
|
|
|
} else { # _NODE_OBLIQUE |
|
3106
|
28791
|
|
|
|
|
41873
|
my ( $idx_arr, $coef_arr, $b ) = ( $node->[1], $node->[2], $node->[3] ); |
|
3107
|
28791
|
|
|
|
|
31892
|
my $coef_off = scalar @coef_idx; |
|
3108
|
28791
|
|
|
|
|
33870
|
my $num = scalar @$idx_arr; |
|
3109
|
|
|
|
|
|
|
|
|
3110
|
|
|
|
|
|
|
# Dense-pack opportunity: when this oblique split uses |
|
3111
|
|
|
|
|
|
|
# every feature (extension_level == n_features - 1 and |
|
3112
|
|
|
|
|
|
|
# all features vary), pack the coefficients in feature |
|
3113
|
|
|
|
|
|
|
# order so val[k] is the coefficient for feature k. The |
|
3114
|
|
|
|
|
|
|
# C scoring path then detects `nf == n_feats` and switches |
|
3115
|
|
|
|
|
|
|
# to a no-gather inner loop (dot += val[k] * xi[k]) that |
|
3116
|
|
|
|
|
|
|
# auto-vectorizes cleanly with FMA. |
|
3117
|
28791
|
100
|
66
|
|
|
57556
|
if ( defined $n_features && $num == $n_features ) { |
|
3118
|
24985
|
|
|
|
|
27570
|
my %coef_for; |
|
3119
|
24985
|
|
|
|
|
53183
|
@coef_for{@$idx_arr} = @$coef_arr; |
|
3120
|
24985
|
|
|
|
|
35144
|
for my $k ( 0 .. $n_features - 1 ) { |
|
3121
|
58167
|
|
|
|
|
78030
|
push @coef_idx, $k; |
|
3122
|
58167
|
|
|
|
|
96450
|
push @coef_val, $coef_for{$k} + 0.0; |
|
3123
|
|
|
|
|
|
|
} |
|
3124
|
|
|
|
|
|
|
} else { |
|
3125
|
3806
|
|
|
|
|
5064
|
for my $i ( 0 .. $num - 1 ) { |
|
3126
|
3811
|
|
|
|
|
5409
|
push @coef_idx, int( $idx_arr->[$i] ); |
|
3127
|
3811
|
|
|
|
|
5812
|
push @coef_val, $coef_arr->[$i] + 0.0; |
|
3128
|
|
|
|
|
|
|
} |
|
3129
|
|
|
|
|
|
|
} |
|
3130
|
|
|
|
|
|
|
|
|
3131
|
28791
|
|
|
|
|
52159
|
my $li = $assign->( $node->[4] ); |
|
3132
|
28791
|
|
|
|
|
37433
|
my $ri = $assign->( $node->[5] ); |
|
3133
|
28791
|
|
|
|
|
60391
|
$node_data[$my_idx] = [ 2.0, $coef_off + 0.0, $num + 0.0, $li + 0.0, $ri + 0.0, $b + 0.0, ]; |
|
3134
|
|
|
|
|
|
|
} ## end else [ if ( $node->[0] == _NODE_LEAF ) ] |
|
3135
|
478238
|
|
|
|
|
578962
|
return $my_idx; |
|
3136
|
4092
|
|
|
|
|
22170
|
}; ## end $assign = sub |
|
3137
|
4092
|
|
|
|
|
8251
|
$assign->($root); |
|
3138
|
|
|
|
|
|
|
|
|
3139
|
4092
|
|
|
|
|
8820
|
my $nodes_packed = pack( 'd*', map { @$_ } @node_data ); |
|
|
478238
|
|
|
|
|
858420
|
|
|
3140
|
4092
|
100
|
|
|
|
58180
|
my $idx_packed = @coef_idx ? pack( 'l*', @coef_idx ) : pack('l*'); |
|
3141
|
4092
|
100
|
|
|
|
8329
|
my $val_packed = @coef_val ? pack( 'd*', @coef_val ) : pack('d*'); |
|
3142
|
4092
|
|
|
|
|
11862
|
return ( $nodes_packed, $idx_packed, $val_packed ); |
|
3143
|
|
|
|
|
|
|
} ## end sub _pack_tree |
|
3144
|
|
|
|
|
|
|
|
|
3145
|
|
|
|
|
|
|
# Build packed C-ready representations for all trees and store them in |
|
3146
|
|
|
|
|
|
|
# $self->{_c_nodes}, $self->{_c_coef_idx}, $self->{_c_coef_val}. |
|
3147
|
|
|
|
|
|
|
# Called after fit() and from_json() when _use_c is true. n_features is |
|
3148
|
|
|
|
|
|
|
# threaded through so _pack_tree can spot the dense-pack opportunity. |
|
3149
|
|
|
|
|
|
|
sub _rebuild_c_trees { |
|
3150
|
92
|
|
|
92
|
|
295
|
my ($self) = @_; |
|
3151
|
92
|
|
|
|
|
239
|
my ( @c_nodes, @c_coef_idx, @c_coef_val ); |
|
3152
|
92
|
|
|
|
|
192
|
for my $tree ( @{ $self->{trees} } ) { |
|
|
92
|
|
|
|
|
325
|
|
|
3153
|
4092
|
|
|
|
|
8713
|
my ( $np, $ip, $vp ) = _pack_tree( $tree, $self->{n_features} ); |
|
3154
|
4092
|
|
|
|
|
7502
|
push @c_nodes, $np; |
|
3155
|
4092
|
|
|
|
|
5850
|
push @c_coef_idx, $ip; |
|
3156
|
4092
|
|
|
|
|
8726
|
push @c_coef_val, $vp; |
|
3157
|
|
|
|
|
|
|
} |
|
3158
|
92
|
|
|
|
|
468
|
$self->{_c_nodes} = \@c_nodes; |
|
3159
|
92
|
|
|
|
|
648
|
$self->{_c_coef_idx} = \@c_coef_idx; |
|
3160
|
92
|
|
|
|
|
296
|
$self->{_c_coef_val} = \@c_coef_val; |
|
3161
|
|
|
|
|
|
|
} ## end sub _rebuild_c_trees |
|
3162
|
|
|
|
|
|
|
|
|
3163
|
|
|
|
|
|
|
sub _check_fitted { |
|
3164
|
56025
|
|
|
56025
|
|
96467
|
my ($self) = @_; |
|
3165
|
|
|
|
|
|
|
croak "model is not fitted yet; call fit() first" |
|
3166
|
56025
|
100
|
66
|
|
|
129702
|
unless ref $self->{trees} eq 'ARRAY' && @{ $self->{trees} }; |
|
|
56025
|
|
|
|
|
165438
|
|
|
3167
|
|
|
|
|
|
|
} |
|
3168
|
|
|
|
|
|
|
|
|
3169
|
|
|
|
|
|
|
# Memoised "does this perl have a real fork()?". False on Windows |
|
3170
|
|
|
|
|
|
|
# without Cygwin; true on every Unix-like platform. |
|
3171
|
|
|
|
|
|
|
{ |
|
3172
|
|
|
|
|
|
|
my $cached; |
|
3173
|
|
|
|
|
|
|
|
|
3174
|
|
|
|
|
|
|
sub _fork_supported { |
|
3175
|
8
|
100
|
|
8
|
|
38
|
return $cached if defined $cached; |
|
3176
|
2
|
|
|
|
|
24
|
require Config; |
|
3177
|
|
|
|
|
|
|
$cached |
|
3178
|
2
|
50
|
50
|
|
|
63
|
= ( ( $Config::Config{d_fork} || '' ) eq 'define' ) ? 1 : 0; |
|
3179
|
2
|
|
|
|
|
11
|
return $cached; |
|
3180
|
|
|
|
|
|
|
} |
|
3181
|
|
|
|
|
|
|
} |
|
3182
|
|
|
|
|
|
|
|
|
3183
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
3184
|
|
|
|
|
|
|
# Fork-based parallel tree builder. Used by fit() when parallel_fit > 1 |
|
3185
|
|
|
|
|
|
|
# and the platform has a real fork(). Divides n_trees evenly among |
|
3186
|
|
|
|
|
|
|
# workers; each child seeds its own RNG ($seed + worker_id * 1009 so |
|
3187
|
|
|
|
|
|
|
# fixed-worker-count runs are reproducible), builds its share (via the |
|
3188
|
|
|
|
|
|
|
# C builder when _use_c is on, same as the non-parallel path), and |
|
3189
|
|
|
|
|
|
|
# returns the trees to the parent via Storable on a one-shot pipe. |
|
3190
|
|
|
|
|
|
|
# |
|
3191
|
|
|
|
|
|
|
# The trees that come back differ from a serial fit with the same seed |
|
3192
|
|
|
|
|
|
|
# because the RNG draws happen in a different order -- this is documented |
|
3193
|
|
|
|
|
|
|
# as part of the parallel_fit contract. |
|
3194
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
3195
|
|
|
|
|
|
|
sub _fit_trees_parallel { |
|
3196
|
8
|
|
|
8
|
|
24
|
my ( $self, $data, $psi, $limit, $workers ) = @_; |
|
3197
|
8
|
|
|
|
|
77
|
require Storable; |
|
3198
|
8
|
|
|
|
|
29
|
require POSIX; |
|
3199
|
|
|
|
|
|
|
|
|
3200
|
8
|
|
|
|
|
32
|
my $n_trees = $self->{n_trees}; |
|
3201
|
8
|
100
|
|
|
|
22
|
$workers = $n_trees if $workers > $n_trees; |
|
3202
|
|
|
|
|
|
|
|
|
3203
|
|
|
|
|
|
|
# Divide n_trees as evenly as possible across workers. |
|
3204
|
8
|
|
|
|
|
15
|
my @shares; |
|
3205
|
|
|
|
|
|
|
{ |
|
3206
|
8
|
|
|
|
|
13
|
my $base = int( $n_trees / $workers ); |
|
|
8
|
|
|
|
|
22
|
|
|
3207
|
8
|
|
|
|
|
17
|
my $extras = $n_trees - $base * $workers; |
|
3208
|
8
|
|
|
|
|
27
|
for my $w ( 0 .. $workers - 1 ) { |
|
3209
|
26
|
100
|
|
|
|
69
|
push @shares, $base + ( $w < $extras ? 1 : 0 ); |
|
3210
|
|
|
|
|
|
|
} |
|
3211
|
|
|
|
|
|
|
} |
|
3212
|
|
|
|
|
|
|
|
|
3213
|
8
|
|
|
|
|
10
|
my @procs; # { pid, rh, share } |
|
3214
|
8
|
|
|
|
|
18
|
for my $w ( 0 .. $workers - 1 ) { |
|
3215
|
26
|
|
|
|
|
174
|
my $share = $shares[$w]; |
|
3216
|
26
|
50
|
|
|
|
175
|
next unless $share > 0; |
|
3217
|
|
|
|
|
|
|
|
|
3218
|
26
|
50
|
|
|
|
2750
|
pipe( my $rh, my $wh ) or croak "pipe failed: $!"; |
|
3219
|
26
|
|
|
|
|
57618
|
my $pid = fork(); |
|
3220
|
26
|
50
|
|
|
|
1802
|
croak "fork failed: $!" unless defined $pid; |
|
3221
|
|
|
|
|
|
|
|
|
3222
|
26
|
50
|
|
|
|
978
|
if ( $pid == 0 ) { |
|
3223
|
|
|
|
|
|
|
# child |
|
3224
|
0
|
|
|
|
|
0
|
close $rh; |
|
3225
|
0
|
|
|
|
|
0
|
binmode $wh; |
|
3226
|
0
|
0
|
|
|
|
0
|
if ( defined $self->{seed} ) { |
|
3227
|
0
|
|
|
|
|
0
|
srand( $self->{seed} + $w * 1009 ); |
|
3228
|
|
|
|
|
|
|
} |
|
3229
|
|
|
|
|
|
|
# Deliberately never _build_forest_openmp here, even when |
|
3230
|
|
|
|
|
|
|
# use_openmp_fit is on: if this process (or the parent that |
|
3231
|
|
|
|
|
|
|
# fork()ed us) already ran any OpenMP region before this |
|
3232
|
|
|
|
|
|
|
# fork -- including plain score_samples()/predict() with |
|
3233
|
|
|
|
|
|
|
# the default use_openmp -- libgomp's thread pool exists |
|
3234
|
|
|
|
|
|
|
# but its worker threads didn't survive the fork. A child |
|
3235
|
|
|
|
|
|
|
# starting its own #pragma omp parallel region then tries |
|
3236
|
|
|
|
|
|
|
# to reuse that now-invalid pool and hangs. This is a |
|
3237
|
|
|
|
|
|
|
# general fork()+libgomp limitation, not fixable from here, |
|
3238
|
|
|
|
|
|
|
# so forked workers always use the single-threaded C |
|
3239
|
|
|
|
|
|
|
# builder (or pure Perl) instead. See t/03-fit-determinism.t |
|
3240
|
|
|
|
|
|
|
# and the NATIVE ACCELERATION docs for the observed hang and |
|
3241
|
|
|
|
|
|
|
# why parallel_fit + use_openmp_fit isn't composed for real. |
|
3242
|
0
|
|
|
|
|
0
|
my $trees; |
|
3243
|
0
|
0
|
|
|
|
0
|
if ( $self->{_use_c} ) { |
|
3244
|
0
|
|
|
|
|
0
|
$trees = $self->_build_forest_c( $data, $psi, $limit, $share ); |
|
3245
|
|
|
|
|
|
|
} else { |
|
3246
|
0
|
|
|
|
|
0
|
my @t; |
|
3247
|
0
|
|
|
|
|
0
|
for ( 1 .. $share ) { |
|
3248
|
0
|
|
|
|
|
0
|
my $sample = _subsample( $data, $psi ); |
|
3249
|
0
|
|
|
|
|
0
|
push @t, $self->_build_tree( $sample, 0, $limit ); |
|
3250
|
|
|
|
|
|
|
} |
|
3251
|
0
|
|
|
|
|
0
|
$trees = \@t; |
|
3252
|
|
|
|
|
|
|
} |
|
3253
|
0
|
|
|
|
|
0
|
print $wh Storable::freeze($trees); |
|
3254
|
0
|
|
|
|
|
0
|
close $wh; |
|
3255
|
|
|
|
|
|
|
# _exit so we don't run parent END/DESTROY in the child. |
|
3256
|
0
|
|
|
|
|
0
|
POSIX::_exit(0); |
|
3257
|
|
|
|
|
|
|
} ## end if ( $pid == 0 ) |
|
3258
|
|
|
|
|
|
|
|
|
3259
|
26
|
|
|
|
|
1742
|
close $wh; |
|
3260
|
26
|
|
|
|
|
499
|
binmode $rh; |
|
3261
|
26
|
|
|
|
|
6300
|
push @procs, { pid => $pid, rh => $rh, share => $share }; |
|
3262
|
|
|
|
|
|
|
} ## end for my $w ( 0 .. $workers - 1 ) |
|
3263
|
|
|
|
|
|
|
|
|
3264
|
|
|
|
|
|
|
# Collect from each pipe in worker order so the canonical tree |
|
3265
|
|
|
|
|
|
|
# ordering is deterministic (worker 0's trees first, then 1's, ...). |
|
3266
|
8
|
|
|
|
|
180
|
my @all_trees; |
|
3267
|
8
|
|
|
|
|
129
|
for my $p (@procs) { |
|
3268
|
26
|
|
|
|
|
126
|
my $buf; |
|
3269
|
|
|
|
|
|
|
{ |
|
3270
|
26
|
|
|
|
|
86
|
local $/; |
|
|
26
|
|
|
|
|
813
|
|
|
3271
|
26
|
|
|
|
|
36850
|
$buf = readline( $p->{rh} ); |
|
3272
|
|
|
|
|
|
|
} |
|
3273
|
26
|
|
|
|
|
382
|
close $p->{rh}; |
|
3274
|
26
|
|
|
|
|
34187
|
waitpid( $p->{pid}, 0 ); |
|
3275
|
26
|
|
|
|
|
290
|
my $exit = $? >> 8; |
|
3276
|
26
|
50
|
|
|
|
157
|
croak "parallel_fit worker $p->{pid} exited with status $exit" |
|
3277
|
|
|
|
|
|
|
if $exit != 0; |
|
3278
|
26
|
|
|
|
|
79
|
my $trees = eval { Storable::thaw($buf) }; |
|
|
26
|
|
|
|
|
401
|
|
|
3279
|
26
|
50
|
33
|
|
|
26590
|
croak "parallel_fit worker $p->{pid} returned unparseable trees: $@" |
|
3280
|
|
|
|
|
|
|
if $@ || ref $trees ne 'ARRAY'; |
|
3281
|
26
|
|
|
|
|
232
|
push @all_trees, @$trees; |
|
3282
|
|
|
|
|
|
|
} ## end for my $p (@procs) |
|
3283
|
|
|
|
|
|
|
|
|
3284
|
8
|
|
|
|
|
286
|
return \@all_trees; |
|
3285
|
|
|
|
|
|
|
} ## end sub _fit_trees_parallel |
|
3286
|
|
|
|
|
|
|
|
|
3287
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
3288
|
|
|
|
|
|
|
# C-accelerated fit(): builds $n_trees trees against $data (a subset or |
|
3289
|
|
|
|
|
|
|
# the full training set) via build_forest_xs, which does its own |
|
3290
|
|
|
|
|
|
|
# per-tree subsampling internally. Random draws inside the C builder |
|
3291
|
|
|
|
|
|
|
# go through Drand01() -- the same generator Perl's rand() uses -- in |
|
3292
|
|
|
|
|
|
|
# the same call order _subsample/_build_tree used, so the returned |
|
3293
|
|
|
|
|
|
|
# trees are bit-identical to what the pure-Perl path would build from |
|
3294
|
|
|
|
|
|
|
# the same RNG state. That's what lets fit() switch backends on the |
|
3295
|
|
|
|
|
|
|
# existing `use_c` knob instead of a new one. |
|
3296
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
3297
|
|
|
|
|
|
|
sub _build_forest_c { |
|
3298
|
58
|
|
|
58
|
|
186
|
my ( $self, $data, $psi, $limit, $n_trees ) = @_; |
|
3299
|
58
|
|
|
|
|
108
|
my $n = scalar @$data; |
|
3300
|
58
|
|
|
|
|
142
|
my $nf = $self->{n_features}; |
|
3301
|
58
|
|
|
|
|
10674
|
my $x_packed = "\0" x ( $n * $nf * 8 ); |
|
3302
|
58
|
|
|
|
|
305
|
my ( $mode, $fill ) = $self->_pack_args; |
|
3303
|
58
|
|
|
|
|
944
|
pack_input_xs( $data, $x_packed, $n, $nf, $mode, $fill ); |
|
3304
|
|
|
|
|
|
|
|
|
3305
|
58
|
100
|
|
|
|
184
|
my $mode_flag = $self->{mode} eq 'extended' ? 1 : 0; |
|
3306
|
58
|
|
100
|
|
|
559
|
my $ext_level = $self->{extension_level_used} // 0; |
|
3307
|
|
|
|
|
|
|
|
|
3308
|
58
|
|
|
|
|
145
|
my $trees = []; |
|
3309
|
58
|
|
|
|
|
233154
|
build_forest_xs( $x_packed, $n, $nf, $n_trees, $psi, $limit, $mode_flag, $ext_level, $trees ); |
|
3310
|
58
|
|
|
|
|
417
|
return $trees; |
|
3311
|
|
|
|
|
|
|
} ## end sub _build_forest_c |
|
3312
|
|
|
|
|
|
|
|
|
3313
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
3314
|
|
|
|
|
|
|
# OpenMP-parallel fit(): builds $n_trees trees across OpenMP threads (one |
|
3315
|
|
|
|
|
|
|
# tree per thread) via build_forest_openmp_xs. Unlike _build_forest_c, |
|
3316
|
|
|
|
|
|
|
# random draws come from a thread-private PRNG seeded per tree index |
|
3317
|
|
|
|
|
|
|
# rather than Drand01() -- Perl's RNG state can't be shared safely |
|
3318
|
|
|
|
|
|
|
# across OpenMP threads -- so the resulting trees are NOT bit-identical |
|
3319
|
|
|
|
|
|
|
# to the use_c (serial) or pure-Perl paths for the same seed, though a |
|
3320
|
|
|
|
|
|
|
# fixed seed + n_trees still reproduce the same trees regardless of |
|
3321
|
|
|
|
|
|
|
# OMP_NUM_THREADS. This is why it's gated by the separate, opt-in |
|
3322
|
|
|
|
|
|
|
# use_openmp_fit knob rather than reusing use_c/use_openmp. |
|
3323
|
|
|
|
|
|
|
# |
|
3324
|
|
|
|
|
|
|
# Only called from fit()'s non-forked branch. _fit_trees_parallel's |
|
3325
|
|
|
|
|
|
|
# workers never call this, even when use_openmp_fit is on: a forked |
|
3326
|
|
|
|
|
|
|
# child starting its own OpenMP region after the parent process has |
|
3327
|
|
|
|
|
|
|
# used OpenMP for anything (this includes plain score_samples()) can |
|
3328
|
|
|
|
|
|
|
# hang -- see the comment above that branch for the fork()+libgomp |
|
3329
|
|
|
|
|
|
|
# hazard this avoids. |
|
3330
|
|
|
|
|
|
|
# |
|
3331
|
|
|
|
|
|
|
# build_forest_openmp_xs hands back three arrayrefs of per-tree packed |
|
3332
|
|
|
|
|
|
|
# buffers (the same SoA layout _pack_tree produces) instead of Perl tree |
|
3333
|
|
|
|
|
|
|
# structures -- that's how it avoids any Perl API call inside its |
|
3334
|
|
|
|
|
|
|
# parallel region. _unpack_forest converts them back into the ordinary |
|
3335
|
|
|
|
|
|
|
# nested-arrayref tree shape so to_json/from_json/_rebuild_c_trees don't |
|
3336
|
|
|
|
|
|
|
# need to know this path exists. |
|
3337
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
3338
|
|
|
|
|
|
|
sub _build_forest_openmp { |
|
3339
|
7
|
|
|
7
|
|
25
|
my ( $self, $data, $psi, $limit, $n_trees ) = @_; |
|
3340
|
7
|
|
|
|
|
12
|
my $n = scalar @$data; |
|
3341
|
7
|
|
|
|
|
23
|
my $nf = $self->{n_features}; |
|
3342
|
7
|
|
|
|
|
332
|
my $x_packed = "\0" x ( $n * $nf * 8 ); |
|
3343
|
7
|
|
|
|
|
34
|
my ( $mode, $fill ) = $self->_pack_args; |
|
3344
|
7
|
|
|
|
|
110
|
pack_input_xs( $data, $x_packed, $n, $nf, $mode, $fill ); |
|
3345
|
|
|
|
|
|
|
|
|
3346
|
7
|
100
|
|
|
|
23
|
my $mode_flag = $self->{mode} eq 'extended' ? 1 : 0; |
|
3347
|
7
|
|
100
|
|
|
26
|
my $ext_level = $self->{extension_level_used} // 0; |
|
3348
|
|
|
|
|
|
|
|
|
3349
|
7
|
|
|
|
|
15
|
my ( @nodes, @idx, @val ); |
|
3350
|
7
|
|
|
|
|
11377
|
build_forest_openmp_xs( $x_packed, $n, $nf, $n_trees, $psi, $limit, |
|
3351
|
|
|
|
|
|
|
$mode_flag, $ext_level, \@nodes, \@idx, \@val, 1 ); |
|
3352
|
|
|
|
|
|
|
|
|
3353
|
7
|
|
|
|
|
64
|
return _unpack_forest( \@nodes, \@idx, \@val ); |
|
3354
|
|
|
|
|
|
|
} ## end sub _build_forest_openmp |
|
3355
|
|
|
|
|
|
|
|
|
3356
|
|
|
|
|
|
|
# Inverse of _pack_tree's SoA layout: given one tree's packed node |
|
3357
|
|
|
|
|
|
|
# buffer plus the shared idx/val coefficient buffers, reconstructs the |
|
3358
|
|
|
|
|
|
|
# ordinary nested-arrayref tree structure _build_tree/_build_node_c |
|
3359
|
|
|
|
|
|
|
# produce. li/ri fields hold the child's absolute node index, so this |
|
3360
|
|
|
|
|
|
|
# just follows them recursively from whatever index the caller says the |
|
3361
|
|
|
|
|
|
|
# root lives at. NOTE: _pack_tree numbers nodes DFS pre-order (root at |
|
3362
|
|
|
|
|
|
|
# 0), but build_forest_openmp_xs appends nodes post-order (children |
|
3363
|
|
|
|
|
|
|
# before parent), putting the root LAST -- the caller must pass the |
|
3364
|
|
|
|
|
|
|
# right root index for the buffer's origin. |
|
3365
|
|
|
|
|
|
|
sub _unpack_node { |
|
3366
|
12844
|
|
|
12844
|
|
16773
|
my ( $nodes, $idx, $val, $node_i ) = @_; |
|
3367
|
12844
|
|
|
|
|
13988
|
my $off = $node_i * 6; |
|
3368
|
12844
|
|
|
|
|
14069
|
my $type = $nodes->[$off]; |
|
3369
|
|
|
|
|
|
|
|
|
3370
|
12844
|
100
|
|
|
|
17170
|
if ( $type == 0 ) { |
|
|
|
100
|
|
|
|
|
|
|
3371
|
6582
|
|
|
|
|
20047
|
return [ _NODE_LEAF, int( $nodes->[ $off + 1 ] ) ]; |
|
3372
|
|
|
|
|
|
|
} elsif ( $type == 1 ) { |
|
3373
|
|
|
|
|
|
|
my ( $attr, $split, $li, $ri ) |
|
3374
|
1604
|
|
|
|
|
1951
|
= @{$nodes}[ $off + 1 .. $off + 4 ]; |
|
|
1604
|
|
|
|
|
2242
|
|
|
3375
|
|
|
|
|
|
|
return [ |
|
3376
|
1604
|
|
|
|
|
2576
|
_NODE_AXIS, int($attr), $split, |
|
3377
|
|
|
|
|
|
|
_unpack_node( $nodes, $idx, $val, int($li) ), |
|
3378
|
|
|
|
|
|
|
_unpack_node( $nodes, $idx, $val, int($ri) ), |
|
3379
|
|
|
|
|
|
|
]; |
|
3380
|
|
|
|
|
|
|
} else { |
|
3381
|
4658
|
|
|
|
|
5782
|
my ( $coff, $num, $li, $ri, $b ) = @{$nodes}[ $off + 1 .. $off + 5 ]; |
|
|
4658
|
|
|
|
|
6568
|
|
|
3382
|
4658
|
|
|
|
|
5487
|
$coff = int($coff); |
|
3383
|
4658
|
|
|
|
|
5010
|
$num = int($num); |
|
3384
|
|
|
|
|
|
|
return [ |
|
3385
|
|
|
|
|
|
|
_NODE_OBLIQUE, |
|
3386
|
4658
|
|
|
|
|
7455
|
[ @{$idx}[ $coff .. $coff + $num - 1 ] ], |
|
3387
|
4658
|
|
|
|
|
5510
|
[ @{$val}[ $coff .. $coff + $num - 1 ] ], |
|
|
4658
|
|
|
|
|
8408
|
|
|
3388
|
|
|
|
|
|
|
$b, |
|
3389
|
|
|
|
|
|
|
_unpack_node( $nodes, $idx, $val, int($li) ), |
|
3390
|
|
|
|
|
|
|
_unpack_node( $nodes, $idx, $val, int($ri) ), |
|
3391
|
|
|
|
|
|
|
]; |
|
3392
|
|
|
|
|
|
|
} ## end else [ if ( $type == 0 ) ] |
|
3393
|
|
|
|
|
|
|
} ## end sub _unpack_node |
|
3394
|
|
|
|
|
|
|
|
|
3395
|
|
|
|
|
|
|
# Unpacks every tree in the three per-tree packed-buffer arrayrefs |
|
3396
|
|
|
|
|
|
|
# build_forest_openmp_xs returns into the ordinary nested tree shape. |
|
3397
|
|
|
|
|
|
|
# The C builder pushes nodes post-order (a node is recorded after both |
|
3398
|
|
|
|
|
|
|
# of its children), so each tree's root is the LAST node record, not |
|
3399
|
|
|
|
|
|
|
# index 0 as in _pack_tree's pre-order layout. |
|
3400
|
|
|
|
|
|
|
sub _unpack_forest { |
|
3401
|
7
|
|
|
7
|
|
24
|
my ( $nodes_list, $idx_list, $val_list ) = @_; |
|
3402
|
7
|
|
|
|
|
13
|
my @trees; |
|
3403
|
7
|
|
|
|
|
28
|
for my $i ( 0 .. $#$nodes_list ) { |
|
3404
|
320
|
|
|
|
|
2230
|
my @nodes = unpack( 'd*', $nodes_list->[$i] ); |
|
3405
|
320
|
|
|
|
|
1031
|
my @idx = unpack( 'l*', $idx_list->[$i] ); |
|
3406
|
320
|
|
|
|
|
958
|
my @val = unpack( 'd*', $val_list->[$i] ); |
|
3407
|
320
|
|
|
|
|
602
|
my $root = @nodes / 6 - 1; |
|
3408
|
320
|
|
|
|
|
560
|
push @trees, _unpack_node( \@nodes, \@idx, \@val, $root ); |
|
3409
|
|
|
|
|
|
|
} |
|
3410
|
7
|
|
|
|
|
62
|
return \@trees; |
|
3411
|
|
|
|
|
|
|
} ## end sub _unpack_forest |
|
3412
|
|
|
|
|
|
|
|
|
3413
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
3414
|
|
|
|
|
|
|
# Packed input wrapper. pack_data() returns one of these so callers can |
|
3415
|
|
|
|
|
|
|
# score the same dataset many times without re-walking the AV/AV refs on |
|
3416
|
|
|
|
|
|
|
# every call -- a meaningful win at high feature counts where |
|
3417
|
|
|
|
|
|
|
# pack_input_xs is a non-trivial slice of total scoring time. |
|
3418
|
|
|
|
|
|
|
# |
|
3419
|
|
|
|
|
|
|
# It's a minimal blessed hashref: { packed, n_pts, n_feats }. The C |
|
3420
|
|
|
|
|
|
|
# scoring functions only need the packed bytes + dimensions. |
|
3421
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
3422
|
|
|
|
|
|
|
sub pack_data { |
|
3423
|
7
|
|
|
7
|
1
|
2049
|
my ( $self, $data ) = @_; |
|
3424
|
7
|
|
|
|
|
29
|
$self->_check_fitted; |
|
3425
|
|
|
|
|
|
|
croak "pack_data requires the Inline::C backend; install Inline::C" |
|
3426
|
6
|
100
|
|
|
|
239
|
unless $self->{_use_c}; |
|
3427
|
5
|
100
|
|
|
|
246
|
croak "pack_data() expects an arrayref of samples" |
|
3428
|
|
|
|
|
|
|
unless ref $data eq 'ARRAY'; |
|
3429
|
3
|
|
|
|
|
5
|
my $n_pts = scalar @$data; |
|
3430
|
3
|
|
|
|
|
6
|
my $nf = $self->{n_features}; |
|
3431
|
3
|
|
|
|
|
18
|
my $x_packed = "\0" x ( $n_pts * $nf * 8 ); |
|
3432
|
3
|
|
|
|
|
16
|
my ( $mode, $fill ) = $self->_pack_args; |
|
3433
|
3
|
|
|
|
|
75
|
pack_input_xs( $data, $x_packed, $n_pts, $nf, $mode, $fill ); |
|
3434
|
3
|
|
|
|
|
33
|
return bless { |
|
3435
|
|
|
|
|
|
|
packed => $x_packed, |
|
3436
|
|
|
|
|
|
|
n_pts => $n_pts, |
|
3437
|
|
|
|
|
|
|
n_feats => $nf, |
|
3438
|
|
|
|
|
|
|
}, |
|
3439
|
|
|
|
|
|
|
'Algorithm::Classifier::IsolationForest::PackedData'; |
|
3440
|
|
|
|
|
|
|
} ## end sub pack_data |
|
3441
|
|
|
|
|
|
|
|
|
3442
|
|
|
|
|
|
|
# Internal helper: given $data that may be a raw arrayref OR a PackedData |
|
3443
|
|
|
|
|
|
|
# instance, return the (n_pts, n_feats, x_packed) triple ready for |
|
3444
|
|
|
|
|
|
|
# score_all_xs. Called from every scoring fast path. |
|
3445
|
|
|
|
|
|
|
sub _resolve_input { |
|
3446
|
55916
|
|
|
55916
|
|
98752
|
my ( $self, $data ) = @_; |
|
3447
|
55916
|
100
|
|
|
|
121329
|
if ( ref $data eq 'Algorithm::Classifier::IsolationForest::PackedData' ) { |
|
3448
|
|
|
|
|
|
|
croak "PackedData has $data->{n_feats} features but model expects " . $self->{n_features} |
|
3449
|
25815
|
100
|
|
|
|
55098
|
unless $data->{n_feats} == $self->{n_features}; |
|
3450
|
25814
|
|
|
|
|
67946
|
return ( $data->{n_pts}, $data->{n_feats}, $data->{packed} ); |
|
3451
|
|
|
|
|
|
|
} |
|
3452
|
30101
|
|
|
|
|
49508
|
my $n_pts = scalar @$data; |
|
3453
|
30101
|
|
|
|
|
52950
|
my $nf = $self->{n_features}; |
|
3454
|
30101
|
|
|
|
|
57861
|
my $x_packed = "\0" x ( $n_pts * $nf * 8 ); |
|
3455
|
30101
|
|
|
|
|
65305
|
my ( $mode, $fill ) = $self->_pack_args; |
|
3456
|
30101
|
|
|
|
|
107045
|
pack_input_xs( $data, $x_packed, $n_pts, $nf, $mode, $fill ); |
|
3457
|
30101
|
|
|
|
|
73415
|
return ( $n_pts, $nf, $x_packed ); |
|
3458
|
|
|
|
|
|
|
} ## end sub _resolve_input |
|
3459
|
|
|
|
|
|
|
|
|
3460
|
|
|
|
|
|
|
# Helper used by the pure-Perl fallback paths: convert either form back |
|
3461
|
|
|
|
|
|
|
# to an arrayref-of-arrayrefs. Slow on PackedData -- the whole point of |
|
3462
|
|
|
|
|
|
|
# packing is to keep things in C -- but lets the fallback path be |
|
3463
|
|
|
|
|
|
|
# uniformly arrayref-driven. |
|
3464
|
|
|
|
|
|
|
sub _to_arrayref { |
|
3465
|
80
|
|
|
80
|
|
147
|
my ( $self, $data ) = @_; |
|
3466
|
80
|
50
|
|
|
|
335
|
return $data if ref $data eq 'ARRAY'; |
|
3467
|
0
|
0
|
|
|
|
0
|
if ( ref $data eq 'Algorithm::Classifier::IsolationForest::PackedData' ) { |
|
3468
|
0
|
|
|
|
|
0
|
my $n_pts = $data->{n_pts}; |
|
3469
|
0
|
|
|
|
|
0
|
my $nf = $data->{n_feats}; |
|
3470
|
0
|
|
|
|
|
0
|
my @doubles = unpack( 'd*', $data->{packed} ); |
|
3471
|
0
|
|
|
|
|
0
|
my @rows; |
|
3472
|
0
|
|
|
|
|
0
|
for my $i ( 0 .. $n_pts - 1 ) { |
|
3473
|
0
|
|
|
|
|
0
|
push @rows, [ @doubles[ $i * $nf .. ( $i + 1 ) * $nf - 1 ] ]; |
|
3474
|
|
|
|
|
|
|
} |
|
3475
|
0
|
|
|
|
|
0
|
return \@rows; |
|
3476
|
|
|
|
|
|
|
} ## end if ( ref $data eq 'Algorithm::Classifier::IsolationForest::PackedData') |
|
3477
|
0
|
|
0
|
|
|
0
|
croak "expected arrayref or PackedData, got " . ( ref($data) || 'scalar' ); |
|
3478
|
|
|
|
|
|
|
} ## end sub _to_arrayref |
|
3479
|
|
|
|
|
|
|
|
|
3480
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
3481
|
|
|
|
|
|
|
# Missing-value handling. |
|
3482
|
|
|
|
|
|
|
# |
|
3483
|
|
|
|
|
|
|
# The `missing` strategy chosen at new() decides how undef feature cells are |
|
3484
|
|
|
|
|
|
|
# treated. Scoring always tolerates undef; the strategy governs fit() and |
|
3485
|
|
|
|
|
|
|
# how undef is represented for the scorer: |
|
3486
|
|
|
|
|
|
|
# |
|
3487
|
|
|
|
|
|
|
# die -- croak from fit() if the training data holds any undef cell. |
|
3488
|
|
|
|
|
|
|
# Scoring still maps undef -> 0 (the long-standing behaviour). |
|
3489
|
|
|
|
|
|
|
# zero -- undef counts as the value 0, at fit and score time. |
|
3490
|
|
|
|
|
|
|
# impute -- undef is replaced by a learned per-feature mean/median; the |
|
3491
|
|
|
|
|
|
|
# fill vector is stored on the model and reused at score time. |
|
3492
|
|
|
|
|
|
|
# nan -- ranges are built over present values only and a point missing |
|
3493
|
|
|
|
|
|
|
# the split feature is routed to the right child, consistently |
|
3494
|
|
|
|
|
|
|
# at fit (Perl) and score (C packs NaN; `<`/`<=` send it right). |
|
3495
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
3496
|
|
|
|
|
|
|
|
|
3497
|
|
|
|
|
|
|
# Returns the training data to actually build trees on, after applying the |
|
3498
|
|
|
|
|
|
|
# missing-value strategy. May croak (die), return a dense filled copy |
|
3499
|
|
|
|
|
|
|
# (zero/impute), or pass $data through unchanged (nan). |
|
3500
|
|
|
|
|
|
|
sub _prepare_fit_data { |
|
3501
|
126
|
|
|
126
|
|
251
|
my ( $self, $data ) = @_; |
|
3502
|
126
|
|
|
|
|
265
|
my $m = $self->{missing}; |
|
3503
|
126
|
|
|
|
|
245
|
my $nf = $self->{n_features}; |
|
3504
|
|
|
|
|
|
|
|
|
3505
|
126
|
100
|
|
|
|
299
|
if ( $m eq 'die' ) { |
|
3506
|
100
|
|
|
|
|
383
|
for my $i ( 0 .. $#$data ) { |
|
3507
|
14706
|
|
|
|
|
16751
|
my $row = $data->[$i]; |
|
3508
|
14706
|
|
|
|
|
18227
|
for my $f ( 0 .. $nf - 1 ) { |
|
3509
|
40681
|
100
|
|
|
|
59586
|
next if defined $row->[$f]; |
|
3510
|
2
|
|
|
|
|
519
|
croak "fit(): undef feature value at sample $i, column $f; " |
|
3511
|
|
|
|
|
|
|
. "construct with missing => 'zero', 'impute', or 'nan' " |
|
3512
|
|
|
|
|
|
|
. "to train on data with missing values"; |
|
3513
|
|
|
|
|
|
|
} |
|
3514
|
|
|
|
|
|
|
} |
|
3515
|
98
|
|
|
|
|
283
|
return $data; |
|
3516
|
|
|
|
|
|
|
} ## end if ( $m eq 'die' ) |
|
3517
|
|
|
|
|
|
|
|
|
3518
|
|
|
|
|
|
|
# nan: leave undef in place -- _build_tree / the split routers handle it. |
|
3519
|
26
|
100
|
|
|
|
68
|
return $data if $m eq 'nan'; |
|
3520
|
|
|
|
|
|
|
|
|
3521
|
|
|
|
|
|
|
# zero / impute: undef has to become a real number somewhere before a |
|
3522
|
|
|
|
|
|
|
# split can look at it. The fill vector is computed either way (it's |
|
3523
|
|
|
|
|
|
|
# needed for persistence and for scoring later), but densifying $data |
|
3524
|
|
|
|
|
|
|
# into a second, fully separate Perl array here is only necessary for |
|
3525
|
|
|
|
|
|
|
# the pure-Perl tree builder (_build_tree assumes every cell is |
|
3526
|
|
|
|
|
|
|
# defined once missing != 'nan' -- see its lo/hi scan). The C |
|
3527
|
|
|
|
|
|
|
# tree-building path -- _build_forest_c/_build_forest_openmp, and |
|
3528
|
|
|
|
|
|
|
# every parallel_fit worker, all of which go through pack_input_xs -- |
|
3529
|
|
|
|
|
|
|
# already fills undef cells itself from this same fill vector, so |
|
3530
|
|
|
|
|
|
|
# skip the redundant whole-dataset copy when that's the path fit() |
|
3531
|
|
|
|
|
|
|
# will actually take. Scoring the training set for a learned |
|
3532
|
|
|
|
|
|
|
# contamination threshold (below, in fit()) is unaffected: it always |
|
3533
|
|
|
|
|
|
|
# runs through the pure-Perl scorer regardless of use_c (fit() drops |
|
3534
|
|
|
|
|
|
|
# any previous fit's packed buffers before that scoring, and |
|
3535
|
|
|
|
|
|
|
# _rebuild_c_trees runs after), and that path already tolerates raw |
|
3536
|
|
|
|
|
|
|
# undef cells |
|
3537
|
|
|
|
|
|
|
# for both zero (_path_length's "// 0") and impute (_prepare_perl_input |
|
3538
|
|
|
|
|
|
|
# densifies on demand from missing_fill). |
|
3539
|
18
|
100
|
|
|
|
223
|
my $fill |
|
3540
|
|
|
|
|
|
|
= $m eq 'impute' |
|
3541
|
|
|
|
|
|
|
? $self->_compute_impute_fill($data) |
|
3542
|
|
|
|
|
|
|
: [ (0) x $nf ]; |
|
3543
|
14
|
100
|
|
|
|
58
|
$self->{missing_fill} = $fill if $m eq 'impute'; |
|
3544
|
14
|
|
|
|
|
53
|
delete $self->{_fill_packed}; |
|
3545
|
|
|
|
|
|
|
|
|
3546
|
14
|
100
|
|
|
|
67
|
return $data if $self->{_use_c}; |
|
3547
|
7
|
|
|
|
|
25
|
return _densify( $data, $fill ); |
|
3548
|
|
|
|
|
|
|
} ## end sub _prepare_fit_data |
|
3549
|
|
|
|
|
|
|
|
|
3550
|
|
|
|
|
|
|
# Per-feature fill value (mean or median of the present values) for impute |
|
3551
|
|
|
|
|
|
|
# mode. Croaks if a feature has no present value to learn from. |
|
3552
|
|
|
|
|
|
|
sub _compute_impute_fill { |
|
3553
|
12
|
|
|
12
|
|
27
|
my ( $self, $data ) = @_; |
|
3554
|
12
|
|
|
|
|
29
|
my $nf = $self->{n_features}; |
|
3555
|
12
|
|
|
|
|
33
|
my $how = $self->{impute_with}; |
|
3556
|
|
|
|
|
|
|
|
|
3557
|
|
|
|
|
|
|
# C fast path: walks the raw data directly and finds the median via |
|
3558
|
|
|
|
|
|
|
# quickselect (O(n) average) instead of the Perl fallback's full sort |
|
3559
|
|
|
|
|
|
|
# (O(n log n)). Produces the same fill values either way -- see |
|
3560
|
|
|
|
|
|
|
# impute_fill_xs's file-top comment -- so use_c only changes speed |
|
3561
|
|
|
|
|
|
|
# here, matching the rest of the module. |
|
3562
|
12
|
100
|
|
|
|
63
|
if ( $self->{_use_c} ) { |
|
3563
|
6
|
|
|
|
|
12
|
my $n = scalar @$data; |
|
3564
|
6
|
100
|
|
|
|
33
|
my $how_flag = $how eq 'median' ? 1 : 0; |
|
3565
|
6
|
|
|
|
|
10
|
my $fill = []; |
|
3566
|
6
|
|
|
|
|
6954
|
impute_fill_xs( $data, $n, $nf, $how_flag, $fill ); |
|
3567
|
4
|
|
|
|
|
23
|
return $fill; |
|
3568
|
|
|
|
|
|
|
} |
|
3569
|
|
|
|
|
|
|
|
|
3570
|
6
|
|
|
|
|
11
|
my @fill; |
|
3571
|
6
|
|
|
|
|
23
|
for my $f ( 0 .. $nf - 1 ) { |
|
3572
|
12
|
|
|
|
|
33
|
my @vals = grep { defined } map { $_->[$f] } @$data; |
|
|
2484
|
|
|
|
|
5610
|
|
|
|
2484
|
|
|
|
|
2889
|
|
|
3573
|
12
|
100
|
|
|
|
587
|
croak "impute: feature column $f has no present values" |
|
3574
|
|
|
|
|
|
|
unless @vals; |
|
3575
|
10
|
100
|
|
|
|
26
|
if ( $how eq 'median' ) { |
|
3576
|
4
|
|
|
|
|
1661
|
my @s = sort { $a <=> $b } @vals; |
|
|
2942
|
|
|
|
|
3726
|
|
|
3577
|
4
|
|
|
|
|
9
|
my $k = scalar @s; |
|
3578
|
4
|
100
|
|
|
|
59
|
$fill[$f] |
|
3579
|
|
|
|
|
|
|
= $k % 2 |
|
3580
|
|
|
|
|
|
|
? $s[ int( $k / 2 ) ] |
|
3581
|
|
|
|
|
|
|
: ( $s[ $k / 2 - 1 ] + $s[ $k / 2 ] ) / 2.0; |
|
3582
|
|
|
|
|
|
|
} else { # mean |
|
3583
|
6
|
|
|
|
|
10
|
my $sum = 0; |
|
3584
|
6
|
|
|
|
|
109
|
$sum += $_ for @vals; |
|
3585
|
6
|
|
|
|
|
34
|
$fill[$f] = $sum / scalar @vals; |
|
3586
|
|
|
|
|
|
|
} |
|
3587
|
|
|
|
|
|
|
} ## end for my $f ( 0 .. $nf - 1 ) |
|
3588
|
4
|
|
|
|
|
15
|
return \@fill; |
|
3589
|
|
|
|
|
|
|
} ## end sub _compute_impute_fill |
|
3590
|
|
|
|
|
|
|
|
|
3591
|
|
|
|
|
|
|
# Return a dense copy of $data with every undef cell replaced by the |
|
3592
|
|
|
|
|
|
|
# matching per-feature fill value. Leaves present cells untouched. |
|
3593
|
|
|
|
|
|
|
sub _densify { |
|
3594
|
10
|
|
|
10
|
|
23
|
my ( $data, $fill ) = @_; |
|
3595
|
10
|
|
|
|
|
18
|
my $nf = scalar @$fill; |
|
3596
|
|
|
|
|
|
|
return [ |
|
3597
|
|
|
|
|
|
|
map { |
|
3598
|
10
|
|
|
|
|
38
|
my $r = $_; |
|
|
1886
|
|
|
|
|
2662
|
|
|
3599
|
1886
|
100
|
|
|
|
2151
|
[ map { defined $r->[$_] ? $r->[$_] : $fill->[$_] } 0 .. $nf - 1 ] |
|
|
4078
|
|
|
|
|
6709
|
|
|
3600
|
|
|
|
|
|
|
} @$data |
|
3601
|
|
|
|
|
|
|
]; |
|
3602
|
|
|
|
|
|
|
} ## end sub _densify |
|
3603
|
|
|
|
|
|
|
|
|
3604
|
|
|
|
|
|
|
# (miss_mode, fill_packed) pair for pack_input_xs, per the active strategy. |
|
3605
|
|
|
|
|
|
|
# die/zero -> 0 (undef becomes 0.0); impute -> 1 (undef becomes fill[k]); |
|
3606
|
|
|
|
|
|
|
# nan -> 2 (undef becomes NaN, which the C scorer routes right). |
|
3607
|
|
|
|
|
|
|
sub _pack_args { |
|
3608
|
30169
|
|
|
30169
|
|
52570
|
my ($self) = @_; |
|
3609
|
30169
|
|
|
|
|
53642
|
my $m = $self->{missing}; |
|
3610
|
30169
|
100
|
|
|
|
64120
|
return ( 2, '' ) if $m eq 'nan'; |
|
3611
|
30160
|
100
|
|
|
|
60949
|
if ( $m eq 'impute' ) { |
|
3612
|
9
|
|
|
|
|
23
|
my $fill = $self->{missing_fill}; |
|
3613
|
|
|
|
|
|
|
croak "impute model is missing its fill vector" |
|
3614
|
9
|
50
|
33
|
|
|
59
|
unless ref $fill eq 'ARRAY' && @$fill == $self->{n_features}; |
|
3615
|
9
|
|
66
|
|
|
60
|
$self->{_fill_packed} //= pack( 'd*', @$fill ); |
|
3616
|
9
|
|
|
|
|
34
|
return ( 1, $self->{_fill_packed} ); |
|
3617
|
|
|
|
|
|
|
} |
|
3618
|
30151
|
|
|
|
|
63884
|
return ( 0, '' ); # die, zero |
|
3619
|
|
|
|
|
|
|
} ## end sub _pack_args |
|
3620
|
|
|
|
|
|
|
|
|
3621
|
|
|
|
|
|
|
# Pure-Perl fallback input prep: arrayref-ify, then fill for impute so the |
|
3622
|
|
|
|
|
|
|
# tree walk sees dense rows. zero/die rely on _path_length's "// 0"; nan |
|
3623
|
|
|
|
|
|
|
# keeps undef in place for _path_length to route. Returns the rows; the |
|
3624
|
|
|
|
|
|
|
# caller passes the nan flag to _path_length separately. |
|
3625
|
|
|
|
|
|
|
sub _prepare_perl_input { |
|
3626
|
59
|
|
|
59
|
|
144
|
my ( $self, $data ) = @_; |
|
3627
|
59
|
|
|
|
|
237
|
my $rows = $self->_to_arrayref($data); |
|
3628
|
59
|
100
|
|
|
|
186
|
if ( $self->{missing} eq 'impute' ) { |
|
3629
|
|
|
|
|
|
|
croak "impute model is missing its fill vector" |
|
3630
|
3
|
50
|
|
|
|
16
|
unless ref $self->{missing_fill} eq 'ARRAY'; |
|
3631
|
3
|
|
|
|
|
13
|
$rows = _densify( $rows, $self->{missing_fill} ); |
|
3632
|
|
|
|
|
|
|
} |
|
3633
|
59
|
|
|
|
|
114
|
return $rows; |
|
3634
|
|
|
|
|
|
|
} ## end sub _prepare_perl_input |
|
3635
|
|
|
|
|
|
|
|
|
3636
|
|
|
|
|
|
|
# Minimal PackedData package: opaque token returned by pack_data(). |
|
3637
|
|
|
|
|
|
|
# Exposes n_pts and n_feats accessors for users who want to introspect. |
|
3638
|
|
|
|
|
|
|
{ |
|
3639
|
|
|
|
|
|
|
|
|
3640
|
|
|
|
|
|
|
package Algorithm::Classifier::IsolationForest::PackedData; |
|
3641
|
4
|
|
|
4
|
|
626
|
sub n_pts { $_[0]->{n_pts} } |
|
3642
|
4
|
|
|
4
|
|
19
|
sub n_feats { $_[0]->{n_feats} } |
|
3643
|
|
|
|
|
|
|
} |
|
3644
|
|
|
|
|
|
|
|
|
3645
|
|
|
|
|
|
|
1; |