Repository navigation
Conversation
Never sort in extra trees. Hoist the column access and gather the column values and targets in find_random_split
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #525 +/- ##
===========================================
+ Coverage 43.97% 86.98% +43.00%
===========================================
Files 85 99 +14
Lines 7281 9898 +2617
===========================================
+ Hits 3202 8610 +5408
+ Misses 4079 1288 -2791 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| in_true_branch: Vec<bool>, // indexed by row index in X | ||
| partition_buffer: Vec<E>, | ||
| sorted_by_feature: Vec<Vec<E>>, | ||
| columns: Vec<Vec<E>>, // either all features, sorted; or only one column (unsorted) |
There was a problem hiding this comment.
how big can be this matrix? please be considerate with allocation, it would be better to use DenseMatrix here so that we keep the zero-copy patterns.
There was a problem hiding this comment.
this was here before this PR. This contains the sorted NodeElements. These are kept as small as possible through the use of different implementations UnitElement, CountedElement, WeightedElement.
In the case of "Best" it contains the same number of rows and columns as X, but it does not contain the actual elements of x.
|
Thanks for this. The approach matches #524, and the 1. Possible allocation regression on the 2. Pre-size the single column 3. Per-node work in
4. Unused partition buffer and an unfinished note 5. One source of truth for the mode 6. Layout of 7. Loop costs
8. Tests and evidence
9. Small points
|
For Splitter::Random, grow() now fills node_targets one time, in the root-statistics loop. unstable_partition() moves the targets together with columns[0]. Thus find_best_cutoff() does not fill the targets again for each node, and split() does not read y. - fit_inner() and grow() do not use presorted columns for Splitter::Random. - Add tests for unstable_partition() and for the random splitter with order = None (constant features in a node, bootstrap counts, sample weights, min_samples_leaf at the boundary).
|
I addressed all of the important points above in the latest commit. These are the results of a hyperfine benchmark run. Extra trees fit:
|
| rows | features | trees | main [s] | branch [s] | speedup |
|---|---|---|---|---|---|
| 1000 | 50 | 100 | 0.054 ± 0.001 | 0.043 ± 0.000 | 1.25 |
| 1000 | 50 | 200 | 0.103 ± 0.003 | 0.086 ± 0.001 | 1.20 |
| 1000 | 50 | 500 | 0.254 ± 0.002 | 0.215 ± 0.004 | 1.18 |
| 1000 | 100 | 100 | 0.103 ± 0.003 | 0.061 ± 0.002 | 1.68 |
| 1000 | 100 | 200 | 0.198 ± 0.004 | 0.118 ± 0.002 | 1.68 |
| 1000 | 100 | 500 | 0.482 ± 0.005 | 0.296 ± 0.005 | 1.63 |
| 1000 | 200 | 100 | 0.211 ± 0.007 | 0.096 ± 0.002 | 2.21 |
| 1000 | 200 | 200 | 0.402 ± 0.019 | 0.176 ± 0.003 | 2.28 |
| 1000 | 200 | 500 | 0.992 ± 0.028 | 0.451 ± 0.013 | 2.20 |
| 5000 | 50 | 100 | 0.260 ± 0.012 | 0.196 ± 0.003 | 1.33 |
| 5000 | 50 | 200 | 0.518 ± 0.020 | 0.383 ± 0.010 | 1.35 |
| 5000 | 50 | 500 | 1.245 ± 0.045 | 0.944 ± 0.019 | 1.32 |
| 5000 | 100 | 100 | 0.496 ± 0.019 | 0.294 ± 0.010 | 1.68 |
| 5000 | 100 | 200 | 0.970 ± 0.035 | 0.590 ± 0.010 | 1.64 |
| 5000 | 100 | 500 | 2.362 ± 0.065 | 1.468 ± 0.021 | 1.61 |
| 5000 | 200 | 100 | 0.946 ± 0.033 | 0.443 ± 0.037 | 2.14 |
| 5000 | 200 | 200 | 1.889 ± 0.071 | 0.878 ± 0.071 | 2.15 |
| 5000 | 200 | 500 | 4.703 ± 0.186 | 2.115 ± 0.067 | 2.22 |
| 10000 | 50 | 100 | 0.529 ± 0.007 | 0.408 ± 0.013 | 1.30 |
| 10000 | 50 | 200 | 1.070 ± 0.040 | 0.850 ± 0.028 | 1.26 |
| 10000 | 50 | 500 | 2.520 ± 0.080 | 2.047 ± 0.075 | 1.23 |
| 10000 | 100 | 100 | 0.991 ± 0.039 | 0.612 ± 0.038 | 1.62 |
| 10000 | 100 | 200 | 1.999 ± 0.121 | 1.195 ± 0.040 | 1.67 |
| 10000 | 100 | 500 | 4.940 ± 0.239 | 3.044 ± 0.166 | 1.62 |
| 10000 | 200 | 100 | 1.846 ± 0.085 | 0.832 ± 0.010 | 2.22 |
| 10000 | 200 | 200 | 3.771 ± 0.710 | 1.622 ± 0.029 | 2.33 |
| 10000 | 200 | 500 | 9.507 ± 0.732 | 4.060 ± 0.357 | 2.34 |
Geometric mean speedup: 1.67 (27 cases). The speedup increases with the number of
features: about 1.25× for 50 features, 1.65× for 100 and 2.2× for 200.
The fitted forests are different from main, because ties between equal splits can be
broken in a different way. The test MSE stays the same (for example 23.170 vs. 23.171 for
10000 rows, 200 features, 500 trees).
The values after "±" are the standard deviations that hyperfine reported.
Fixes #524
Checklist
Current behaviour
Extra trees (
Splitter::Random) use the same data layout as random forests (Splitter::Best):Extra trees do not need sorted data. They draw a random threshold between the minimum and the maximum of the feature. Thus the sort and the partition of the
plists are extra work. This work increases with the number of features.New expected behaviour
For
Splitter::Random:orderis nowOption<&[Vec<usize>]>and isNonefor extra trees.columns[0]). After a split, the tree partitions only this list.find_random_splitgathers the values of the feature and the targets of the node into reusable buffers. It then gets the minimum and the maximum in one pass, and counts the true branch without a sorted early exit.splitassigns each row to the true or false branch in one pass over the unsorted list.For
Splitter::Best(random forests, decision trees) there is no change in behaviour. Apresortedflag inSplitWorkspaceselects the old code path.The predictions of extra trees are not bit-identical to
main, but they have the same statistical behaviour. The sklearn parity tests ofExtraTreesRegressorpass with no change in reference values or tolerances.Changed
BaseTreefit functions takeorder: Option<&[Vec<usize>]>(crate internal)