From 7eafaa79eea9b57337be96b31cfae671726049e2 Mon Sep 17 00:00:00 2001 From: Ryan Curtin Date: Thu, 1 May 2025 15:47:54 -0400 Subject: [PATCH] Document `SpillTree` and its four variants (#3925) * Commit initial tested documentation of spill tree. * Document all four variants of spill trees. * Correct documentation: a rho of 0.5 will result in no overlapping. * Add new trees to sidebar. * Don't upgrade pip since it is installed by Homebrew on OS X. --- .github/actions/binding_setup/action.yml | 1 - doc/sidebar.html | 25 + doc/user/core/trees.md | 12 +- doc/user/core/trees/ball_tree.md | 6 +- doc/user/core/trees/binary_space_tree.md | 7 +- doc/user/core/trees/cover_tree.md | 6 +- doc/user/core/trees/hilbert_r_tree.md | 2 +- doc/user/core/trees/kdtree.md | 6 +- doc/user/core/trees/max_rp_tree.md | 14 +- doc/user/core/trees/mean_sp_tree.md | 754 ++++++++++++ doc/user/core/trees/mean_split_ball_tree.md | 6 +- doc/user/core/trees/mean_split_kdtree.md | 6 +- doc/user/core/trees/non_ort_mean_sp_tree.md | 746 +++++++++++ doc/user/core/trees/non_ort_sp_tree.md | 742 +++++++++++ doc/user/core/trees/octree.md | 5 +- doc/user/core/trees/r_plus_plus_tree.md | 2 +- doc/user/core/trees/r_plus_tree.md | 2 +- doc/user/core/trees/r_star_tree.md | 2 +- doc/user/core/trees/r_tree.md | 2 +- doc/user/core/trees/rectangle_tree.md | 4 +- doc/user/core/trees/rp_tree.md | 13 +- doc/user/core/trees/sp_tree.md | 749 ++++++++++++ doc/user/core/trees/spill_tree.md | 1086 +++++++++++++++++ doc/user/core/trees/ub_tree.md | 6 +- doc/user/core/trees/vptree.md | 6 +- doc/user/core/trees/x_tree.md | 2 +- .../core/tree/space_split/hyperplane.hpp | 34 +- .../space_split/mean_space_split_impl.hpp | 4 +- .../space_split/midpoint_space_split_impl.hpp | 2 +- .../tree/space_split/projection_vector.hpp | 11 +- .../core/tree/space_split/space_split.hpp | 8 +- .../tree/space_split/space_split_impl.hpp | 30 +- .../core/tree/spill_tree/is_spill_tree.hpp | 2 +- .../spill_tree/spill_dual_tree_traverser.hpp | 3 +- .../spill_dual_tree_traverser_impl.hpp | 6 +- .../spill_single_tree_traverser.hpp | 3 +- .../spill_single_tree_traverser_impl.hpp | 6 +- .../core/tree/spill_tree/spill_tree.hpp | 29 +- .../core/tree/spill_tree/spill_tree_impl.hpp | 129 +- src/mlpack/core/tree/spill_tree/traits.hpp | 3 +- src/mlpack/core/tree/spill_tree/typedef.hpp | 16 +- src/mlpack/tests/hyperplane_test.cpp | 10 +- 42 files changed, 4322 insertions(+), 186 deletions(-) create mode 100644 doc/user/core/trees/mean_sp_tree.md create mode 100644 doc/user/core/trees/non_ort_mean_sp_tree.md create mode 100644 doc/user/core/trees/non_ort_sp_tree.md create mode 100644 doc/user/core/trees/sp_tree.md create mode 100644 doc/user/core/trees/spill_tree.md diff --git a/.github/actions/binding_setup/action.yml b/.github/actions/binding_setup/action.yml index 9bbdddcf5a..16f3c05ff6 100644 --- a/.github/actions/binding_setup/action.yml +++ b/.github/actions/binding_setup/action.yml @@ -26,7 +26,6 @@ runs: if: inputs.lang == 'Python' && runner.os == 'macOS' shell: bash run: | - /opt/homebrew/bin/python3 -m pip install --break-system-packages --upgrade pip /opt/homebrew/bin/python3 -m pip install --break-system-packages setuptools cython pandas zipp configparser wheel pytest echo "CMAKE_BINDING_ARGS=-DPYTHON_EXECUTABLE=/opt/homebrew/bin/python3" >> $GITHUB_ENV diff --git a/doc/sidebar.html b/doc/sidebar.html index 85bb8edffa..c13dea7e9d 100644 --- a/doc/sidebar.html +++ b/doc/sidebar.html @@ -190,6 +190,31 @@ when the sidebar is built for each page. RectangleTree +
  • + + SPTree + +
  • +
  • + + MeanSPTree + +
  • +
  • + + NonOrtSPTree + +
  • +
  • + + NonOrtMeanSPTree + +
  • +
  • + + SpillTree + +
  • diff --git a/doc/user/core/trees.md b/doc/user/core/trees.md index dc5443cca1..d259a8a728 100644 --- a/doc/user/core/trees.md +++ b/doc/user/core/trees.md @@ -16,10 +16,13 @@ different trees. The following tree types are available in mlpack: * [`MeanSplitBallTree`](trees/mean_split_ball_tree.md) * [`RPTree`](trees/rp_tree.md) * [`MaxRPTree`](trees/max_rp_tree.md) - * [`BinarySpaceTree`](trees/binary_space_tree.md) * [`UBTree`](trees/ub_tree.md) + * [`BinarySpaceTree`](trees/binary_space_tree.md) + * [`CoverTree`](trees/cover_tree.md) + * [`Octree`](trees/octree.md) + * [`RTree`](trees/r_tree.md) * [`RStarTree`](trees/r_star_tree.md) * [`XTree`](trees/x_tree.md) @@ -28,8 +31,11 @@ different trees. The following tree types are available in mlpack: * [`HilbertRTree`](trees/hilbert_r_tree.md) * [`RectangleTree`](trees/rectangle_tree.md) -*Note:* this documentation is a work in progress. Not all trees are documented -yet. + * [`SPTree`](trees/sp_tree.md) + * [`MeanSPTree`](trees/mean_sp_tree.md) + * [`NonOrtSPTree`](trees/non_ort_sp_tree.md) + * [`NonOrtMeanSPTree`](trees/non_ort_mean_sp_tree.md) + * [`SpillTree`](trees/spill_tree.md) --- diff --git a/doc/user/core/trees/ball_tree.md b/doc/user/core/trees/ball_tree.md index fc5150f380..2519391c92 100644 --- a/doc/user/core/trees/ball_tree.md +++ b/doc/user/core/trees/ball_tree.md @@ -142,13 +142,11 @@ different. loose bounding balls. It is better to simply build a new `BallTree` on the modified dataset. For trees that support individual insertion and deletions, see the [`RectangleTree`](rectangle_tree.md) class and all its variants (e.g. - [`RTree`](r_tree.md), `RStarTree`, etc.). + [`RTree`](r_tree.md), [`RStarTree`](r_star_tree.md), etc.). - See also the [developer documentation on tree constructors](../../../developer/trees.md#constructors-and-destructors). - - --- ### Constructor parameters: @@ -433,7 +431,7 @@ mlpack::data::Load("cloud.csv", dataset, true); // // Note that the '<>' isn't necessary if C++20 is being used (e.g. // `mlpack::BallTree tree(...)` will work fine in C++20 or newer). -mlpack::BallTree<> tree(std::move(dataset)); +mlpack::BallTree<> tree(std::move(dataset), 10); // Print the bounding ball of the root node. std::cout << "Bounding ball of root node:" << std::endl; diff --git a/doc/user/core/trees/binary_space_tree.md b/doc/user/core/trees/binary_space_tree.md index f26d7b0df0..1e634de6b6 100644 --- a/doc/user/core/trees/binary_space_tree.md +++ b/doc/user/core/trees/binary_space_tree.md @@ -168,13 +168,12 @@ different. with very loose bounding boxes. It is better to simply build a new `BinarySpaceTree` on the modified dataset. For trees that support individual insertion and deletions, see the [`RectangleTree`](rectangle_tree.md) class - and all its variants (e.g. [`RTree`](r_tree.md), `RStarTree`, etc.). + and all its variants (e.g. [`RTree`](r_tree.md), + [`RStarTree`](r_star_tree.md), etc.). - See also the [developer documentation on tree constructors](../../../developer/trees.md#constructors-and-destructors). - - --- ### Constructor parameters: @@ -2240,7 +2239,7 @@ mlpack::BinarySpaceTree tree(std::move(dataset)); + mlpack::MidpointSplit> tree(std::move(dataset), 10); // Print the bounding box of the root node. std::cout << "Bounding box of root node:" << std::endl; diff --git a/doc/user/core/trees/cover_tree.md b/doc/user/core/trees/cover_tree.md index 44311f87ab..8238e7e17c 100644 --- a/doc/user/core/trees/cover_tree.md +++ b/doc/user/core/trees/cover_tree.md @@ -101,14 +101,12 @@ dataset. is not supported, because this generally results in a cover tree with very loose bounding balls. It is better to simply build a new `CoverTree` on the modified dataset. For trees that support individual insertion and deletions, - see the `RectangleTree` class and all its variants (e.g. `RTree`, - `RStarTree`, etc.). + see the [`RectangleTree`](rectangle_tree.md) class and all its variants (e.g. + [`RTree`](r_tree.md), [`RStarTree`](r_star_tree.md), etc.). - See also the [developer documentation on tree constructors](../../../developer/trees.md#constructors-and-destructors). - - --- ### Constructor parameters: diff --git a/doc/user/core/trees/hilbert_r_tree.md b/doc/user/core/trees/hilbert_r_tree.md index 8e507b964c..45b7683515 100644 --- a/doc/user/core/trees/hilbert_r_tree.md +++ b/doc/user/core/trees/hilbert_r_tree.md @@ -153,7 +153,7 @@ The dataset is not permuted during the construction process. | **name** | **type** | **description** | **default** | |----------|----------|-----------------|-------------| -| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. Pass with `std::move(data)` to avoid copying the matrix. | _(N/A)_ | +| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. | _(N/A)_ | | `maxLeafSize` | `size_t` | Maximum number of points to store in each leaf. | `20` | | `minLeafSize` | `size_t` | Minimum number of points to store in each leaf. | `8` | | `maxNumChildren` | `size_t` | Maximum number of children allowed in each non-leaf node. | `5` | diff --git a/doc/user/core/trees/kdtree.md b/doc/user/core/trees/kdtree.md index 44a547a6ec..e3659f77d3 100644 --- a/doc/user/core/trees/kdtree.md +++ b/doc/user/core/trees/kdtree.md @@ -126,13 +126,11 @@ different. bounding boxes. It is better to simply build a new `KDTree` on the modified dataset. For trees that support individual insertion and deletions, see the [`RectangleTree`](rectangle_tree.md) class and all its variants (e.g. - [`RTree`](r_tree.md), `RStarTree`, etc.). + [`RTree`](r_tree.md), [`RStarTree`](r_star_tree.md), etc.). - See also the [developer documentation on tree constructors](../../../developer/trees.md#constructors-and-destructors). - - --- ### Constructor parameters: @@ -421,7 +419,7 @@ mlpack::data::Load("cloud.csv", dataset, true); // // Note that the '<>' isn't necessary if C++20 is being used (e.g. // `mlpack::KDTree tree(...)` will work fine in C++20 or newer). -mlpack::KDTree<> tree(std::move(dataset)); +mlpack::KDTree<> tree(std::move(dataset), 10); // Print the bounding box of the root node. std::cout << "Bounding box of root node:" << std::endl; diff --git a/doc/user/core/trees/max_rp_tree.md b/doc/user/core/trees/max_rp_tree.md index 3fb8d87364..20ed12de12 100644 --- a/doc/user/core/trees/max_rp_tree.md +++ b/doc/user/core/trees/max_rp_tree.md @@ -9,10 +9,9 @@ nearest neighbor search) in low dimensions---typically less than 100. An `MaxRPTree` (or the similar [`RPTree`](rp_tree.md)) may be preferred over a [`KDTree`](kdtree.md) or other tree structures as it is theoretically known to -adapt to the intrinsic dimension of the data. This is similar to the cover -tree, but the implementation is far simpler and as a result, more efficient. - - +adapt to the intrinsic dimension of the data. This is similar to the +[cover tree](cover_tree.md), but the implementation is far simpler and as a +result, more efficient. mlpack's `MaxRPTree` implementation supports three template parameters for configurable behavior, and implements all the functionality required by the @@ -137,13 +136,12 @@ different. with very loose bounding boxes. It is better to simply build a new `MaxRPTree` on the modified dataset. For trees that support individual insertion and deletions, see the [`RectangleTree`](rectangle_tree.md) class - and all its variants (e.g. [`RTree`](r_tree.md), `RStarTree`, etc.). + and all its variants (e.g. [`RTree`](r_tree.md), + [`RStarTree`](r_star_tree.md), etc.). - See also the [developer documentation on tree constructors](../../../developer/trees.md#constructors-and-destructors). - - --- ### Constructor parameters: @@ -432,7 +430,7 @@ mlpack::data::Load("cloud.csv", dataset, true); // // Note that the '<>' isn't necessary if C++20 is being used (e.g. // `mlpack::MaxRPTree tree(...)` will work fine in C++20 or newer). -mlpack::MaxRPTree<> tree(std::move(dataset)); +mlpack::MaxRPTree<> tree(std::move(dataset), 10); // Print the bounding box of the root node. std::cout << "Bounding box of root node:" << std::endl; diff --git a/doc/user/core/trees/mean_sp_tree.md b/doc/user/core/trees/mean_sp_tree.md new file mode 100644 index 0000000000..39b6abdaba --- /dev/null +++ b/doc/user/core/trees/mean_sp_tree.md @@ -0,0 +1,754 @@ +# `MeanSPTree` + +The `MeanSPTree` class implements the mean-split hybrid spill tree, a binary +space partitioning tree that allows overlapping volumes between nodes. This +type of tree can be more effective than trees like the [`KDTree`](kdtree.md) for +approximate nearest neighbor search and related tasks. `MeanSPTree` is the same +tree as [`SPTree`](sp_tree.md), except nodes are split using the mean value of +data points projected onto the splitting hyperplane's tangent vector +([`SPTree`](sp_tree.md) instead uses the midpoint). + +`MeanSPTree` supports three template parameters for configurable behavior, and +implements all the functionality required by the [TreeType +API](../../../developer/trees.md#the-treetype-api), plus some additional +functionality specific to spill trees. `MeanSPTree` is built on the more +generic [`SpillTree`](spill_tree.md) class, so if fully custom behavior is +desired, that + + * [Template parameters](#template-parameters) + * [Constructors](#constructors) + * [Basic tree properties](#basic-tree-properties) + * [Bounding distances with the tree](#bounding-distances-with-the-tree) + * [Tree traversals](#tree-traversals) + * [Example usage](#example-usage) + +## See also + + + + * [`SpillTree`](spill_tree.md) + * [`SPTree`](sp_tree.md) + * [`NonOrtSPTree`](non_ort_sp_tree.md) + * [`NonOrtMeanSPTree`](non_ort_mean_sp_tree.md) + * [`BinarySpaceTree`](binary_space_tree.md) + * [An Investigation of Practical Approximate Nearest Neighbor Algorithms (pdf)](https://proceedings.neurips.cc/paper/2004/file/1102a326d5f7c9e04fc3c89d0ede88c9-Paper.pdf) + * [Tree-Independent Dual-Tree Algorithms (pdf)](https://www.ratml.org/pub/pdf/2013tree.pdf) + +## Template parameters + +In accordance with the +[TreeType API](../../../developer/trees.md#template-parameters-required-by-the-treetype-policy) +(see also +[this more detailed section](../../../developer/trees.md#template-parameters)), +the `MeanSPTree` class takes three template parameters: + +``` +MeanSPTree +``` + + * `DistanceType`: the [distance metric](../distances.md) to use for distance + computations. Because the `MeanSPTree` internally uses + [`HRectBound`](binary_space_tree.md#hrectbound), this is required to be + [`EuclideanDistance`](../distances.md#lmetric). See + [`NonOrtMeanSPTree`](non_ort_mean_sp_tree.md) for a version of the mean-split + spill tree where arbitrary distance metrics are allowed. + + * `StatisticType`: this holds auxiliary information in each tree node. By + default, [`EmptyStatistic`](binary_space_tree.md#emptystatistic) is used, + which holds no information. + - See the [`StatisticType`](binary_space_tree.md#statistictype) section in + the `BinarySpaceTree` documentation for more details. + + * `MatType`: the type of matrix used to represent points. Must be a type + matching the [Armadillo API](../../matrices.md). By default, `arma::mat` is + used, but other types such as `arma::fmat` or similar will work just fine. + +The `MeanSPTree` class itself is a convenience typedef of the generic +[`SpillTree`](spill_tree.md) class, using the +[`AxisOrthogonalHyperplane`](spill_tree.md#axisorthogonalhyperplane) class as +the splitting hyperplane type, and the +[`MeanSpaceSplit`](spill_tree.md#meanspacesplit) class as the splitting +strategy. + +If no template parameters are explicitly specified, then defaults are used: + +``` +MeanSPTree<> = MeanSPTree +``` + +## Constructors + +`MeanSPTree`s are constructed by iteratively finding splitting hyperplanes, and +points within a margin of the hyperplane are assigned to *both* child nodes. +Unlike the constructors of +[`BinarySpaceTree`](binary_space_tree.md#constructors), the dataset is not +permuted during construction. + +--- + + * `node = MeanSPTree(data, tau=0.0, maxLeafSize=20, rho=0.7)` + - Construct a `MeanSPTree` on the given `data`, using the specified + hyperparameters to control tree construction behavior. + - By default, a reference to `data` is stored. If `data` goes out of scope + after tree construction, memory errors will occur! To avoid this, either + pass the dataset or a copy with `std::move()` (e.g. `std::move(data)`); + when doing this, `data` will be set to an empty matrix. + +--- + + * `node = MeanSPTree(data, tau=0.0, maxLeafSize=20, rho=0.7)` + - Construct a `MeanSPTree` on the given `data`, using custom template + parameters, and using the specified hyperparameters to control tree + construction behavior. + - By default, a reference to `data` is stored. If `data` goes out of scope + after tree construction, memory errors will occur! To avoid this, either + pass the dataset or a copy with `std::move()` (e.g. `std::move(data)`); + when doing this, `data` will be set to an empty matrix. + +--- + + * `node = MeanSPTree()` + - Construct an empty `MeanSPTree` with no children, no points, and default + template parameters. + +--- + +***Notes:*** + + - The name `node` is used here for `MeanSPTree` objects instead of `tree`, + because each `MeanSPTree` object is a single node in the tree. The + constructor returns the node that is the root of the tree. + + - Inserting individual points or removing individual points from a `MeanSPTree` + is not supported, because this generally results in a tree with very + suboptimal hyperplane splits. It is better to simply build a new + `MeanSPTree` on the modified dataset. For trees that support individual + insertion and deletions, see the [`RectangleTree`](rectangle_tree.md) class + and all its variants (e.g. [`RTree`](r_tree.md), + [`RStarTree`](r_star_tree.md), etc.). + + - See also the + [developer documentation on tree constructors](../../../developer/trees.md#constructors-and-destructors). + +--- + +### Constructor parameters: + +| **name** | **type** | **description** | **default** | +|----------|----------|-----------------|-------------| +| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. | _(N/A)_ | +| `tau` | `double` | Width of spill margin: points within `tau` of the splitting hyperplane of a node will be contained in both left and right children. | `0.0` | +| `maxLeafSize` | `size_t` | Maximum number of points to store in each leaf. | `20` | +| `rho` | `double` | Balance threshold. When splitting, if either overlapping node would contain a fraction of more than `rho` of the points, a non-overlapping split is performed. Must be in the range `[0.0, 1.0)`. | `0.7` | + +***Caveats***: + + * `tau` must be manually tuned for the properties of each dataset; the default, + `0.0`, will never allow overlap between nodes (and thus the created tree will + essentially be a non-overlapping [`BinarySpaceTree`](binary_space_tree.md)). + + * If `tau` is set too large, nodes will overlap too much and search quality + will be degraded. + + * `rho` implicitly controls the depth of the tree by forcing very overlapping + children to be non-overlapping. As `rho` gets closer to `1`, more overlap is + allowed, which in turn makes the tree deeper. If `rho` is set to `0.5` or + less, then all splits will be non-overlapping (and the tree will essentially + be a [`BinarySpaceTree`](binary_space_tree.md)). + +## Basic tree properties + +Once a `MeanSPTree` object is constructed, various properties of the tree can be +accessed or inspected. Many of these functions are required by the +[TreeType API](../../../developer/trees.md#the-treetype-api). + +### Navigating the tree + + * `node.NumChildren()` returns the number of children in `node`. This is + either `2` if `node` has children, or `0` if `node` is a leaf. + + * `node.IsLeaf()` returns a `bool` indicating whether or not `node` is a leaf. + + * `node.Child(i)` returns an `MeanSPTree&` that is the `i`th child. + - `i` must be `0` or `1`. + - This function should only be called if `node.NumChildren()` is not `0` + (e.g. if `node` is not a leaf). Note that this returns a valid + `MeanSPTree&` that can itself be used just like the root node of the + tree! + - `node.Left()` and `node.Right()` are convenience functions specific to + `MeanSPTree` that will return `MeanSPTree*` (pointers) to the left and + right children, respectively, or `NULL` if `node` has no children. + + * `node.Parent()` will return a `MeanSPTree*` that points to the parent of + `node`, or `NULL` if `node` is the root of the `MeanSPTree`. + +--- + +### Accessing members of a tree + + * `node.Overlap()` will return a `bool` that is `true` if `node`'s children are + overlapping, and `false` otherwise. + + * `node.Hyperplane()` will return an + [`AxisOrthogonalHyperplane`](spill_tree.md#axisorthogonalhyperplane) object + that represents the axis-aligned splitting hyperplane of `node`. + - All points in `node.Left()` are to the left of `node.Hyperplane()` if + `node.Overlap()` is `false`; otherwise, all points in `node.Left()` are to + the left of `node.Hyperplane() + tau`. + - All points in `node.Right()` are to the right of `node.Hyperplane()` if + `node.Overlap()` is `false`; otherwise, all points in `node.Right()` are to + the right of `node.Hyperplane() - tau`. + + * `node.Bound()` will return a + [`const HRectBound&`](binary_space_tree.md#hrectbound) representing the + bounding box associated with `node`. + - If a [custom `DistanceType` and/or `MatType`](#template-parameters) are + specified, then a `const HRectBound&` is returned. + * `ElemType` is the element type of the specified `MatType` (e.g. `double` + for `arma::mat`, `float` for `arma::fmat`, etc.). + + * `node.Stat()` will return a `StatisticType&` holding the statistics of the + node that were computed during tree construction. + + * `node.Distance()` will return a `EuclideanDistance&`. Because + `EuclideanDistance` has no instantiated members, this is unlikely to be + useful, but is required to satisfy the + [`TreeType` API](../../../developer/trees.md#the-treetype-api). + +See also the +[developer documentation](../../../developer/trees.md#basic-tree-functionality) +for basic tree functionality in mlpack. + +--- + +### Accessing data held in a tree + + * `node.Dataset()` will return a `const MatType&` that is the dataset the + tree was built on. + + * `node.NumPoints()` returns a `size_t` indicating the number of points held + directly in `node`. + - If `node` is not a leaf, this will return `0`, as `MeanSPTree` only holds + points directly in its leaves. + - If `node` is a leaf, then the number of points will be less than or equal + to the `maxLeafSize` that was specified when the tree was constructed. + + * `node.Point(i)` returns a `size_t` indicating the index of the `i`'th point + in `node.Dataset()`. + - `i` must be in the range `[0, node.NumPoints() - 1]` (inclusive). + - `node` must be a leaf (as non-leaves do not hold any points). + - The `i`'th point in `node` can then be accessed as + `node.Dataset().col(node.Point(i))`. + - Accessing the actual `i`'th point itself can be done with, e.g., + `node.Dataset().col(node.Point(i))`. + + * `node.NumDescendants()` returns a `size_t` indicating the number of points + held in all descendant leaves of `node`. + - If `node` is the root of the tree, then `node.NumDescendants()` will be + equal to `node.Dataset().n_cols`. + + * `node.Descendant(i)` returns a `size_t` indicating the index of the `i`'th + descendant point in `node.Dataset()`. + - `i` must be in the range `[0, node.NumDescendants() - 1]` (inclusive). + - `node` does not need to be a leaf. + - The `i`'th descendant point in `node` can then be accessed as + `node.Dataset().col(node.Descendant(i))`. + - Accessing the actual `i`'th descendant itself can be done with, e.g., + `node.Dataset().col(node.Descendant(i))`. + +--- + +### Accessing computed bound quantities of a tree + +The following quantities are cached for each node in a `MeanSPTree`, and so +accessing them does not require any computation. In the documentation below, +`ElemType` is the element type of the given `MatType`; e.g., if `MatType` is +`arma::mat`, then `ElemType` is `double`. + + * `node.FurthestPointDistance()` returns an `ElemType` representing the + distance between the center of the bound of `node` and the furthest point + held by `node`. + - If `node` is not a leaf, this returns 0 (because `node` does not hold any + points). + + * `node.FurthestDescendantDistance()` returns an `ElemType` representing the + distance between the center of the bound of `node` and the furthest + descendant point held by `node`. + + * `node.MinimumBoundDistance()` returns an `ElemType` representing the minimum + possible distance from the center of the node to any edge of its bound. + + * `node.ParentDistance()` returns an `ElemType` representing the distance + between the center of the bound of `node` and the center of the bound of its + parent. + - If `node` is the root of the tree, `0` is returned. + +***Note:*** for more details on each bound quantity, see the [developer +documentation](../../../developer/trees.md#complex-tree-functionality-and-bounds) +on bound quantities for trees. + +--- + +### Other functionality + + * `node.Center(center)` computes the center of the bound of `node` and stores + it in `center`. + - `center` should be of type `arma::Col&`, where `ElemType` is the + element type of the specified `MatType`. + - `center` will be set to have size equivalent to the dimensionality of the + dataset held by `node`. + - This is equivalent to calling `node.Bound().Center(center)`. + + * A `MeanSPTree` can be serialized with + [`data::Save()` and `data::Load()`](../../load_save.md#mlpack-objects). + +## Bounding distances with the tree + +The primary use of trees in mlpack is bounding distances to points or other tree +nodes. The following functions can be used for these tasks. + + * `node.GetNearestChild(point)` + * `node.GetFurthestChild(point)` + - Return a `size_t` indicating the index of the child (`0` for left, `1` for + right) that is closest to (or furthest from) `point`, with respect + to the `MinDistance()` (or `MaxDistance()`) function. + - If there is a tie, `0` (the left child) is returned. + - If `node` is a leaf, `0` is returned. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + + * `node.GetNearestChild(other)` + * `node.GetFurthestChild(other)` + - Return a `size_t` indicating the index of the child (`0` for left, `1` for + right) that is closest to (or furthest from) the `MeanSPTree` node `other`, + with respect to the `MinDistance()` (or `MaxDistance()`) function. + - If there is a tie, `0` (the left child) is returned. + - If `node` is a leaf, `0` is returned. + +--- + + * `node.MinDistance(point)` + * `node.MinDistance(other)` + - Return a `double` indicating the minimum possible distance between `node` + and `point`, or the `MeanSPTree` node `other`. + - This is equivalent to the minimum possible distance between any point + contained in the bounding hyperrectangle of `node` and `point`, or between + any point contained in the bounding hyperrectangle of `node` and any point + contained in the bounding hyperrectangle of `other`. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + + * `node.MaxDistance(point)` + * `node.MaxDistance(other)` + - Return a `double` indicating the maximum possible distance between `node` + and `point`, or the `MeanSPTree` node `other`. + - This is equivalent to the maximum possible distance between any point + contained in the bounding hyperrectangle of `node` and `point`, or between + any point contained in the bounding hyperrectangle of `node` and any point + contained in the bounding hyperrectangle of `other`. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + + * `node.RangeDistance(point)` + * `node.RangeDistance(other)` + - Return a [`RangeType`](../math.md#range) whose lower bound is + `node.MinDistance(point)` or `node.MinDistance(other)`, and whose upper + bound is `node.MaxDistance(point)` or `node.MaxDistance(other)`. + - `ElemType` is the element type of `MatType`. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + +## Tree traversals + +Like every mlpack tree, the `MeanSPTree` class provides a [single-tree and +dual-tree traversal](../../../developer/trees.md#traversals) that can be paired +with a [`RuleType` class](../../../developer/trees.md#rules) to implement a +single-tree or dual-tree algorithm. + + * `MeanSPTree::SingleTreeTraverser` + - Implements a depth-first single-tree traverser. + + * `MeanSPTree::DualTreeTraverser` + - Implements a dual-depth-first dual-tree traverser. + +However, spill trees are primarily useful because the overlapping nodes allow +*defeatist* search to be effective. Defeatist search is non-backtracking: the +tree is traversed to one leaf only. For example, finding the approximate +nearest neighbor of a point `p` with defeatist search is done by recursing in +the tree, choosing the child with smallest minimum distance to `p`, and when a +leaf is encountered, choosing the closest point in the leaf to `p` as the +nearest neighbor. This is the strategy used in the +[original spill tree paper (pdf)](https://proceedings.neurips.cc/paper/2004/file/1102a326d5f7c9e04fc3c89d0ede88c9-Paper.pdf). + +Defeatist traversers, matching the API for a regular +[traversal](../../../developer/trees.md#traversals) are made available as the +following two classes: + + * `MeanSPTree::DefeatistSingleTreeTraverser` + - Implements a depth-first single-tree defeatist traverser with no + backtracking. Traversal will terminate after the first leaf is visited. + + * `MeanSPTree::DefeatistDualTreeTraverser` + - Implements a dual-depth-first dual-tree defeatist traversal with no + backtracking. For each query leaf node, traversal will terminate after the + first reference leaf node is visited. + +Any [`RuleType`](../../../developer/trees.md#rules) that is being used with a +defeatist traversal, in addition to the functions required by the `RuleType` +API, must implement the following functions: + +``` +// This is only required for single-tree defeatist traversals. +// It should return the index of the branch that should be chosen for the given +// query point and reference node. +template +size_t GetBestChild(const VecType& queryPoint, TreeType& referenceNode); + +// This is only required for dual-tree defeatist traversals. +// It should return the index of the best child of the reference node that +// should be chosen for the given query node. +template +size_t GetBestChild(TreeType& queryNode, TreeType& referenceNode); + +// Return the minimum number of base cases (point-to-point computations) that +// are required during the traversal. +size_t MinimumBaseCases(); +``` + +## Example usage + +Build a `MeanSPTree` on the `cloud` dataset and print basic statistics about the +tree. + +```c++ +// See https://datasets.mlpack.org/cloud.csv. +arma::mat dataset; +mlpack::data::Load("cloud.csv", dataset, true); + +// Build the mean-split spill tree with a tau (margin) of 0.2 and a leaf size of +// 10. (This means that nodes are split until they contain 10 or fewer points.) +// +// The std::move() means that `dataset` will be empty after this call, and no +// data will be copied during tree building. +// +// When C++20 is enabled, then the <> is not necessary and the following line +// will work: +// mlpack::MeanSPTree tree(std::move(dataset), 0.2, 10); +mlpack::MeanSPTree<> tree(std::move(dataset), 0.2, 10); + +// Print the bounding box of the root node. +std::cout << "Bounding box of root node:" << std::endl; +for (size_t i = 0; i < tree.Bound().Dim(); ++i) +{ + std::cout << " - Dimension " << i << ": [" << tree.Bound()[i].Lo() << ", " + << tree.Bound()[i].Hi() << "]." << std::endl; +} +std::cout << std::endl; + +// Print the number of descendant points of the root, and of each of its +// children. +std::cout << "Descendant points of root: " + << tree.NumDescendants() << "." << std::endl; +std::cout << "Descendant points of left child: " + << tree.Left()->NumDescendants() << "." << std::endl; +std::cout << "Descendant points of right child: " + << tree.Right()->NumDescendants() << "." << std::endl; +std::cout << std::endl; + +// Compute the center of the MeanSPTree. +arma::vec center; +tree.Center(center); +std::cout << "Center of tree: " << center.t(); +``` + +--- + +Build two `MeanSPTree`s on subsets of the corel dataset and compute minimum and +maximum distances between different nodes in the tree. + +```c++ +// See https://datasets.mlpack.org/corel-histogram.csv. +arma::mat dataset; +mlpack::data::Load("corel-histogram.csv", dataset, true); + +// Build trees on the first half and the second half of points. Use a tau +// (overlap) parameter of 0.3, which is tuned to this dataset, and a rho value +// of 0.6 to prevent the trees getting too deep. +mlpack::MeanSPTree<> tree1(dataset.cols(0, dataset.n_cols / 2), 0.3, 20, 0.6); +mlpack::MeanSPTree<> tree2(dataset.cols(dataset.n_cols / 2 + 1, + dataset.n_cols - 1), 0.3, 20, 0.6); + +// Compute the maximum distance between the trees. +std::cout << "Maximum distance between tree root nodes: " + << tree1.MaxDistance(tree2) << "." << std::endl; + +// Get the leftmost grandchild of the first tree's root---if it exists. +if (!tree1.IsLeaf() && !tree1.Child(0).IsLeaf()) +{ + mlpack::MeanSPTree<>& node1 = tree1.Child(0).Child(0); + + // Get the rightmost grandchild of the second tree's root---if it exists. + if (!tree2.IsLeaf() && !tree2.Child(1).IsLeaf()) + { + mlpack::MeanSPTree<>& node2 = tree2.Child(1).Child(1); + + // Print the minimum and maximum distance between the nodes. + mlpack::Range dists = node1.RangeDistance(node2); + std::cout << "Possible distances between two grandchild nodes: [" + << dists.Lo() << ", " << dists.Hi() << "]." << std::endl; + + // Print the minimum distance between the first node and the first + // descendant point of the second node. + const size_t descendantIndex = node2.Descendant(0); + const double descendantMinDist = + node1.MinDistance(node2.Dataset().col(descendantIndex)); + std::cout << "Minimum distance between grandchild node and descendant " + << "point: " << descendantMinDist << "." << std::endl; + + // Which child of node2 is closer to node1? + const size_t closerIndex = node2.GetNearestChild(node1); + if (closerIndex == 0) + std::cout << "The left child of node2 is closer to node1." << std::endl; + else if (closerIndex == 1) + std::cout << "The right child of node2 is closer to node1." << std::endl; + else // closerIndex == 2 in this case. + std::cout << "Both children of node2 are equally close to node1." + << std::endl; + + // And which child of node1 is further from node2? + const size_t furtherIndex = node1.GetFurthestChild(node2); + if (furtherIndex == 0) + std::cout << "The left child of node1 is further from node2." + << std::endl; + else if (furtherIndex == 1) + std::cout << "The right child of node1 is further from node2." + << std::endl; + else // furtherIndex == 2 in this case. + std::cout << "Both children of node1 are equally far from node2." + << std::endl; + } +} +``` + +--- + +Build a `MeanSPTree` on 32-bit floating point data and save it to disk. + +```c++ +// See https://datasets.mlpack.org/corel-histogram.csv. +arma::fmat dataset; +mlpack::data::Load("corel-histogram.csv", dataset); + +// Build the MeanSPTree using 32-bit floating point data as the matrix type. +// We will still use the default EmptyStatistic and EuclideanDistance +// parameters. +mlpack::MeanSPTree tree(std::move(dataset), 0.1, 20, 0.95); + +// Save the tree to disk with the name 'tree'. +mlpack::data::Save("tree.bin", "tree", tree); + +std::cout << "Saved tree with " << tree.Dataset().n_cols << " points to " + << "'tree.bin'." << std::endl; +``` + +--- + +Load a 32-bit floating point `MeanSPTree` from disk, then traverse it manually +and find the number of nodes whose children overlap. + +```c++ +// This assumes the tree has already been saved to 'tree.bin' (as in the example +// above). + +// This convenient typedef saves us a long type name! +using TreeType = mlpack::MeanSPTree; + +TreeType tree; +mlpack::data::Load("tree.bin", "tree", tree); +std::cout << "Tree loaded with " << tree.NumDescendants() << " points." + << std::endl; + +// Recurse in a depth-first manner. Count both the total number of non-leaves, +// and the number of non-leaves that have overlapping children. +size_t overlapCount = 0; +size_t totalInternalNodeCount = 0; +std::stack stack; +stack.push(&tree); +while (!stack.empty()) +{ + TreeType* node = stack.top(); + stack.pop(); + + if (node->IsLeaf()) + continue; + + if (node->Overlap()) + ++overlapCount; + ++totalInternalNodeCount; + + stack.push(node->Left()); + stack.push(node->Right()); +} + +// Note that it would be possible to use TreeType::SingleTreeTraverser to +// perform the recursion above, but that is more well-suited for more complex +// tasks that require pruning and other non-trivial behavior; so using a simple +// stack is the better option here. + +// Print the results. +std::cout << overlapCount << " out of " << totalInternalNodeCount + << " internal nodes have overlapping children." << std::endl; +``` + +--- + +Use a defeatist traversal to find the approximate nearest neighbor of the third +and fourth points in the `corel-histogram` dataset. (Note: this can also be +done more easily with the `KNN` class! This example is a demonstration of how +to use the defeatist traverser.) + + + +For this example, we must first define a +[`RuleType` class](../../../developer/trees.md#rules). + +```c++ +// For simplicity, this only implements those methods required by single-tree +// traversals, and cannot be used with a dual-tree traversal. +// +// `.Reset()` must be called before any additional single-tree traversals after +// the first is run. +class SpillNearestNeighborRule +{ + public: + // Store the dataset internally. + SpillNearestNeighborRule(const arma::mat& dataset) : + dataset(dataset), + nearestNeighbor(size_t(-1)), + nearestDistance(DBL_MAX) { } + + // Compute the base case (point-to-point comparison). + double BaseCase(const size_t queryIndex, const size_t referenceIndex) + { + // Skip the base case if the points are the same. + if (queryIndex == referenceIndex) + return 0.0; + + const double dist = mlpack::EuclideanDistance::Evaluate( + dataset.col(queryIndex), dataset.col(referenceIndex)); + + if (dist < nearestDistance) + { + nearestNeighbor = referenceIndex; + nearestDistance = dist; + } + + return dist; + } + + // Score the given node in the tree; if it is sufficiently far away that it + // cannot contain a better nearest neighbor candidate, we can prune it. + template + double Score(const size_t queryIndex, const TreeType& referenceNode) const + { + const double minDist = referenceNode.MinDistance(dataset.col(queryIndex)); + if (minDist > nearestDistance) + return DBL_MAX; // Prune: this cannot contain a better candidate! + + return minDist; + } + + // Rescore the given node/point combination. Note that this will not be used + // by the defeatist traversal as it never backtracks, but we include it for + // completeness because the RuleType API requires it. + template + double Rescore(const size_t, const TreeType&, const double oldScore) const + { + if (oldScore > nearestDistance) + return DBL_MAX; // Prune: the node is too far away. + return oldScore; + } + + // This is required by defeatist traversals to select the best reference + // child to recurse into for overlapping nodes. + template + size_t GetBestChild(const size_t queryIndex, TreeType& referenceNode) + const + { + return referenceNode.GetNearestChild(dataset.col(queryIndex)); + } + + // We must perform at least two base cases in order to have a result. Note + // that this is two, and not one, because we skip base cases where the query + // and reference points are the same. That can only happen a maximum of once, + // so to ensure that we compare a query point to a different reference point + // at least once, we must return 2 here. + size_t MinimumBaseCases() const { return 2; } + + // Get the results (to be called after the traversal). + size_t NearestNeighbor() const { return nearestNeighbor; } + double NearestDistance() const { return nearestDistance; } + + // Reset the internal statistics for an additional traversal. + void Reset() + { + nearestNeighbor = size_t(-1); + nearestDistance = DBL_MAX; + } + + private: + const arma::mat& dataset; + + size_t nearestNeighbor; + double nearestDistance; +}; +``` + +```c++ +// See https://datasets.mlpack.org/corel-histogram.csv. +arma::mat dataset; +mlpack::data::Load("corel-histogram.csv", dataset, true); + +// Build two trees, one with a lot of overlap, and one with no overlap +// (e.g. tau = 0). +mlpack::MeanSPTree<> tree1(dataset, 0.5, 10), tree2(dataset, 0.0, 10); + +// Construct the rule types, and then the traversals. +SpillNearestNeighborRule r1(dataset), r2(dataset); + +mlpack::MeanSPTree<>::DefeatistSingleTreeTraverser + t1(r1), t2(r2); + +// Search for the approximate nearest neighbor of point 3 using both trees. +t1.Traverse(3, tree1); +t2.Traverse(3, tree2); + +std::cout << "Approximate nearest neighbor of point 3:" << std::endl; +std::cout << " - Mean-split spill tree with overlap 0.5 found: point " + << r1.NearestNeighbor() << ", distance " << r1.NearestDistance() + << "." << std::endl; + +std::cout << " - Mean-split spill tree with no overlap found: point " + << r2.NearestNeighbor() << ", distance " << r2.NearestDistance() + << "." << std::endl; + +// Now search for point 6. +r1.Reset(); +r2.Reset(); + +t1.Traverse(6, tree1); +t2.Traverse(6, tree2); + +std::cout << "Approximate nearest neighbor of point 6:" << std::endl; +std::cout << " - Mean-split spill tree with overlap 0.5 found: point " + << r1.NearestNeighbor() << ", distance " << r1.NearestDistance() + << "." << std::endl; + +std::cout << " - Mean-split spill tree with no overlap found: point " + << r2.NearestNeighbor() << ", distance " << r2.NearestDistance() + << "." << std::endl; +``` diff --git a/doc/user/core/trees/mean_split_ball_tree.md b/doc/user/core/trees/mean_split_ball_tree.md index 88a5d91a82..50b63b09cf 100644 --- a/doc/user/core/trees/mean_split_ball_tree.md +++ b/doc/user/core/trees/mean_split_ball_tree.md @@ -140,13 +140,11 @@ may be different. `MeanSplitBallTree` on the modified dataset. For trees that support individual insertion and deletions, see the [`RectangleTree`](rectangle_tree.md) class and all its variants (e.g. - [`RTree`](r_tree.md), `RStarTree`, etc.). + [`RTree`](r_tree.md), [`RStarTree`](r_star_tree.md), etc.). - See also the [developer documentation on tree constructors](../../../developer/trees.md#constructors-and-destructors). - - --- ### Constructor parameters: @@ -432,7 +430,7 @@ mlpack::data::Load("cloud.csv", dataset, true); // // Note that the '<>' isn't necessary if C++20 is being used (e.g. // `mlpack::MeanSplitBallTree tree(...)` will work fine in C++20 or newer). -mlpack::MeanSplitBallTree<> tree(std::move(dataset)); +mlpack::MeanSplitBallTree<> tree(std::move(dataset), 10); // Print the bounding box of the root node. std::cout << "Bounding ball of root node:" << std::endl; diff --git a/doc/user/core/trees/mean_split_kdtree.md b/doc/user/core/trees/mean_split_kdtree.md index 0710ea1a8f..a6fdde5d55 100644 --- a/doc/user/core/trees/mean_split_kdtree.md +++ b/doc/user/core/trees/mean_split_kdtree.md @@ -137,13 +137,11 @@ different. build a new `MeanSplitKDTree` on the modified dataset. For trees that support individual insertion and deletions, see the [`RectangleTree`](rectangle_tree.md) class and all its variants (e.g. - [`RTree`](r_tree.md), `RStarTree`, etc.). + [`RTree`](r_tree.md), [`RStarTree`](r_star_tree.md), etc.). - See also the [developer documentation on tree constructors](../../../developer/trees.md#constructors-and-destructors). - - --- ### Constructor parameters: @@ -434,7 +432,7 @@ mlpack::data::Load("cloud.csv", dataset, true); // // Note that the '<>' isn't necessary if C++20 is being used (e.g. // `mlpack::MeanSplitKDTree tree(...)` will work fine in C++20 or newer). -mlpack::MeanSplitKDTree<> tree(std::move(dataset)); +mlpack::MeanSplitKDTree<> tree(std::move(dataset), 10); // Print the bounding box of the root node. std::cout << "Bounding box of root node:" << std::endl; diff --git a/doc/user/core/trees/non_ort_mean_sp_tree.md b/doc/user/core/trees/non_ort_mean_sp_tree.md new file mode 100644 index 0000000000..c67ec54362 --- /dev/null +++ b/doc/user/core/trees/non_ort_mean_sp_tree.md @@ -0,0 +1,746 @@ +# `NonOrtMeanSPTree` + +The `NonOrtMeanSPTree` class implements the hybrid spill tree with +non-axis-orthogonal splitting hyperplanes and mean-split behavior; this is a +binary space partitioning tree that allows overlapping volumes between nodes. +This type of tree can be more effective than trees like the +[`KDTree`](kdtree.md) for approximate nearest neighbor search and related tasks. + +`NonOrtMeanSPTree` supports three template parameters for configurable behavior, +and implements all the functionality required by the +[TreeType API](../../../developer/trees.md#the-treetype-api), plus some +additional functionality specific to spill trees. `NonOrtMeanSPTree` is built +on the more generic [`SpillTree`](spill_tree.md) class, so if fully custom +behavior is desired, that + + * [Template parameters](#template-parameters) + * [Constructors](#constructors) + * [Basic tree properties](#basic-tree-properties) + * [Bounding distances with the tree](#bounding-distances-with-the-tree) + * [Tree traversals](#tree-traversals) + * [Example usage](#example-usage) + +## See also + + + + * [`SpillTree`](spill_tree.md) + * [`SPTree`](sp_tree.md) + * [`MeanSPTree`](mean_sp_tree.md) + * [`NonOrtSPTree`](non_ort_sp_tree.md) + * [`BinarySpaceTree`](binary_space_tree.md) + * [An Investigation of Practical Approximate Nearest Neighbor Algorithms (pdf)](https://proceedings.neurips.cc/paper/2004/file/1102a326d5f7c9e04fc3c89d0ede88c9-Paper.pdf) + * [Tree-Independent Dual-Tree Algorithms (pdf)](https://www.ratml.org/pub/pdf/2013tree.pdf) + +## Template parameters + +In accordance with the +[TreeType API](../../../developer/trees.md#template-parameters-required-by-the-treetype-policy) +(see also +[this more detailed section](../../../developer/trees.md#template-parameters)), +the `NonOrtMeanSPTree` class takes three template parameters: + +``` +NonOrtMeanSPTree +``` + + * `DistanceType`: the [distance metric](../distances.md) to use for distance + computations. + + * `StatisticType`: this holds auxiliary information in each tree node. By + default, [`EmptyStatistic`](binary_space_tree.md#emptystatistic) is used, + which holds no information. + - See the [`StatisticType`](binary_space_tree.md#statistictype) section in + the `BinarySpaceTree` documentation for more details. + + * `MatType`: the type of matrix used to represent points. Must be a type + matching the [Armadillo API](../../matrices.md). By default, `arma::mat` is + used, but other types such as `arma::fmat` or similar will work just fine. + +The `NonOrtMeanSPTree` class itself is a convenience typedef of the generic +[`SpillTree`](spill_tree.md) class, using the +[`Hyperplane`](spill_tree.md#hyperplane) class as the splitting hyperplane type, +and the [`MeanSpaceSplit`](spill_tree.md#meanspacesplit) class as the splitting +strategy. + +If no template parameters are explicitly specified, then defaults are used: + +``` +NonOrtMeanSPTree<> = NonOrtMeanSPTree +``` + +## Constructors + +`NonOrtMeanSPTree`s are constructed by iteratively finding splitting +hyperplanes, and points within a margin of the hyperplane are assigned to *both* +child nodes. Unlike the constructors of +[`BinarySpaceTree`](binary_space_tree.md#constructors), the dataset is not +permuted during construction. + +--- + + * `node = NonOrtMeanSPTree(data, tau=0.0, maxLeafSize=20, rho=0.7)` + - Construct a `NonOrtMeanSPTree` on the given `data`, using the specified + hyperparameters to control tree construction behavior. + - By default, a reference to `data` is stored. If `data` goes out of scope + after tree construction, memory errors will occur! To avoid this, either + pass the dataset or a copy with `std::move()` (e.g. `std::move(data)`); + when doing this, `data` will be set to an empty matrix. + +--- + + * `node = NonOrtMeanSPTree(data, tau=0.0, maxLeafSize=20, rho=0.7)` + - Construct a `NonOrtMeanSPTree` on the given `data`, using custom template + parameters, and using the specified hyperparameters to control tree + construction behavior. + - By default, a reference to `data` is stored. If `data` goes out of scope + after tree construction, memory errors will occur! To avoid this, either + pass the dataset or a copy with `std::move()` (e.g. `std::move(data)`); + when doing this, `data` will be set to an empty matrix. + +--- + + * `node = NonOrtMeanSPTree()` + - Construct an empty `NonOrtMeanSPTree` with no children, no points, and + default template parameters. + +--- + +***Notes:*** + + - The name `node` is used here for `NonOrtMeanSPTree` objects instead of + `tree`, because each `NonOrtMeanSPTree` object is a single node in the tree. + The constructor returns the node that is the root of the tree. + + - Inserting individual points or removing individual points from a + `NonOrtMeanSPTree` is not supported, because this generally results in a tree + with very suboptimal hyperplane splits. It is better to simply build a new + `NonOrtMeanSPTree` on the modified dataset. For trees that support + individual insertion and deletions, see the + [`RectangleTree`](rectangle_tree.md) class and all its variants (e.g. + [`RTree`](r_tree.md), [`RStarTree`](r_star_tree.md), etc.). + + - See also the + [developer documentation on tree constructors](../../../developer/trees.md#constructors-and-destructors). + +--- + +### Constructor parameters: + +| **name** | **type** | **description** | **default** | +|----------|----------|-----------------|-------------| +| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. | _(N/A)_ | +| `tau` | `double` | Width of spill margin: points within `tau` of the splitting hyperplane of a node will be contained in both left and right children. | `0.0` | +| `maxLeafSize` | `size_t` | Maximum number of points to store in each leaf. | `20` | +| `rho` | `double` | Balance threshold. When splitting, if either overlapping node would contain a fraction of more than `rho` of the points, a non-overlapping split is performed. Must be in the range `[0.0, 1.0)`. | `0.7` | + +***Caveats***: + + * `tau` must be manually tuned for the properties of each dataset; the default, + `0.0`, will never allow overlap between nodes (and thus the created tree will + essentially be a non-overlapping [`BinarySpaceTree`](binary_space_tree.md)). + + * If `tau` is set too large, nodes will overlap too much and search quality + will be degraded. + + * `rho` implicitly controls the depth of the tree by forcing very overlapping + children to be non-overlapping. As `rho` gets closer to `1`, more overlap is + allowed, which in turn makes the tree deeper. If `rho` is set to `0.5` or + less, then all splits will be non-overlapping (and the tree will essentially + be a [`BinarySpaceTree`](binary_space_tree.md)). + +## Basic tree properties + +Once an `NonOrtMeanSPTree` object is constructed, various properties of the tree +can be accessed or inspected. Many of these functions are required by the +[TreeType API](../../../developer/trees.md#the-treetype-api). + +### Navigating the tree + + * `node.NumChildren()` returns the number of children in `node`. This is + either `2` if `node` has children, or `0` if `node` is a leaf. + + * `node.IsLeaf()` returns a `bool` indicating whether or not `node` is a leaf. + + * `node.Child(i)` returns a `NonOrtMeanSPTree&` that is the `i`th child. + - `i` must be `0` or `1`. + - This function should only be called if `node.NumChildren()` is not `0` + (e.g. if `node` is not a leaf). Note that this returns a valid + `NonOrtMeanSPTree&` that can itself be used just like the root node of the + tree! + - `node.Left()` and `node.Right()` are convenience functions specific to + `NonOrtMeanSPTree` that will return `NonOrtMeanSPTree*` (pointers) to the + left and right children, respectively, or `NULL` if `node` has no children. + + * `node.Parent()` will return an `NonOrtMeanSPTree*` that points to the parent + of `node`, or `NULL` if `node` is the root of the `NonOrtMeanSPTree`. + +--- + +### Accessing members of a tree + + * `node.Overlap()` will return a `bool` that is `true` if `node`'s children are + overlapping, and `false` otherwise. + + * `node.Hyperplane()` will return an [`Hyperplane`](spill_tree.md#hyperplane) + object that represents the splitting hyperplane of `node`. + - All points in `node.Left()` are to the left of `node.Hyperplane()` if + `node.Overlap()` is `false`; otherwise, all points in `node.Left()` are to + the left of `node.Hyperplane() + tau`. + - All points in `node.Right()` are to the right of `node.Hyperplane()` if + `node.Overlap()` is `false`; otherwise, all points in `node.Right()` are to + the right of `node.Hyperplane() - tau`. + + * `node.Bound()` will return a + [`const BallBound&`](binary_space_tree.md#ballbound) representing the + bounding box associated with `node`. + - If a [custom `DistanceType` and/or `MatType`](#template-parameters) are + specified, then a `const BallBound&` is returned. + * `ElemType` is the element type of the specified `MatType` (e.g. `double` + for `arma::mat`, `float` for `arma::fmat`, etc.). + + * `node.Stat()` will return a `StatisticType&` holding the statistics of the + node that were computed during tree construction. + + * `node.Distance()` will return a `DistanceType&` that can be used to make + distance computations. + +See also the +[developer documentation](../../../developer/trees.md#basic-tree-functionality) +for basic tree functionality in mlpack. + +--- + +### Accessing data held in a tree + + * `node.Dataset()` will return a `const MatType&` that is the dataset the + tree was built on. + + * `node.NumPoints()` returns a `size_t` indicating the number of points held + directly in `node`. + - If `node` is not a leaf, this will return `0`, as `NonOrtMeanSPTree` only + holds points directly in its leaves. + - If `node` is a leaf, then the number of points will be less than or equal + to the `maxLeafSize` that was specified when the tree was constructed. + + * `node.Point(i)` returns a `size_t` indicating the index of the `i`'th point + in `node.Dataset()`. + - `i` must be in the range `[0, node.NumPoints() - 1]` (inclusive). + - `node` must be a leaf (as non-leaves do not hold any points). + - The `i`'th point in `node` can then be accessed as + `node.Dataset().col(node.Point(i))`. + - Accessing the actual `i`'th point itself can be done with, e.g., + `node.Dataset().col(node.Point(i))`. + + * `node.NumDescendants()` returns a `size_t` indicating the number of points + held in all descendant leaves of `node`. + - If `node` is the root of the tree, then `node.NumDescendants()` will be + equal to `node.Dataset().n_cols`. + + * `node.Descendant(i)` returns a `size_t` indicating the index of the `i`'th + descendant point in `node.Dataset()`. + - `i` must be in the range `[0, node.NumDescendants() - 1]` (inclusive). + - `node` does not need to be a leaf. + - The `i`'th descendant point in `node` can then be accessed as + `node.Dataset().col(node.Descendant(i))`. + - Accessing the actual `i`'th descendant itself can be done with, e.g., + `node.Dataset().col(node.Descendant(i))`. + +--- + +### Accessing computed bound quantities of a tree + +The following quantities are cached for each node in a `NonOrtMeanSPTree`, and +so accessing them does not require any computation. In the documentation below, +`ElemType` is the element type of the given `MatType`; e.g., if `MatType` is +`arma::mat`, then `ElemType` is `double`. + + * `node.FurthestPointDistance()` returns an `ElemType` representing the + distance between the center of the bound of `node` and the furthest point + held by `node`. + - If `node` is not a leaf, this returns 0 (because `node` does not hold any + points). + + * `node.FurthestDescendantDistance()` returns an `ElemType` representing the + distance between the center of the bound of `node` and the furthest + descendant point held by `node`. + + * `node.MinimumBoundDistance()` returns an `ElemType` representing the minimum + possible distance from the center of the node to any edge of its bound. + + * `node.ParentDistance()` returns an `ElemType` representing the distance + between the center of the bound of `node` and the center of the bound of its + parent. + - If `node` is the root of the tree, `0` is returned. + +***Note:*** for more details on each bound quantity, see the [developer +documentation](../../../developer/trees.md#complex-tree-functionality-and-bounds) +on bound quantities for trees. + +--- + +### Other functionality + + * `node.Center(center)` computes the center of the bound of `node` and stores + it in `center`. + - `center` should be of type `arma::Col&`, where `ElemType` is the + element type of the specified `MatType`. + - `center` will be set to have size equivalent to the dimensionality of the + dataset held by `node`. + - This is equivalent to calling `node.Bound().Center(center)`. + + * A `NonOrtMeanSPTree` can be serialized with + [`data::Save()` and `data::Load()`](../../load_save.md#mlpack-objects). + +## Bounding distances with the tree + +The primary use of trees in mlpack is bounding distances to points or other tree +nodes. The following functions can be used for these tasks. + + * `node.GetNearestChild(point)` + * `node.GetFurthestChild(point)` + - Return a `size_t` indicating the index of the child (`0` for left, `1` for + right) that is closest to (or furthest from) `point`, with respect + to the `MinDistance()` (or `MaxDistance()`) function. + - If there is a tie, `0` (the left child) is returned. + - If `node` is a leaf, `0` is returned. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + + * `node.GetNearestChild(other)` + * `node.GetFurthestChild(other)` + - Return a `size_t` indicating the index of the child (`0` for left, `1` for + right) that is closest to (or furthest from) the `NonOrtMeanSPTree` node + `other`, with respect to the `MinDistance()` (or `MaxDistance()`) function. + - If there is a tie, `0` (the left child) is returned. + - If `node` is a leaf, `0` is returned. + +--- + + * `node.MinDistance(point)` + * `node.MinDistance(other)` + - Return a `double` indicating the minimum possible distance between `node` + and `point`, or the `NonOrtMeanSPTree` node `other`. + - This is equivalent to the minimum possible distance between any point + contained in the bounding hyperrectangle of `node` and `point`, or between + any point contained in the bounding hyperrectangle of `node` and any point + contained in the bounding hyperrectangle of `other`. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + + * `node.MaxDistance(point)` + * `node.MaxDistance(other)` + - Return a `double` indicating the maximum possible distance between `node` + and `point`, or the `NonOrtMeanSPTree` node `other`. + - This is equivalent to the maximum possible distance between any point + contained in the bounding hyperrectangle of `node` and `point`, or between + any point contained in the bounding hyperrectangle of `node` and any point + contained in the bounding hyperrectangle of `other`. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + + * `node.RangeDistance(point)` + * `node.RangeDistance(other)` + - Return a [`RangeType`](../math.md#range) whose lower bound is + `node.MinDistance(point)` or `node.MinDistance(other)`, and whose upper + bound is `node.MaxDistance(point)` or `node.MaxDistance(other)`. + - `ElemType` is the element type of `MatType`. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + +## Tree traversals + +Like every mlpack tree, the `NonOrtMeanSPTree` class provides a [single-tree and +dual-tree traversal](../../../developer/trees.md#traversals) that can be paired +with a [`RuleType` class](../../../developer/trees.md#rules) to implement a +single-tree or dual-tree algorithm. + + * `NonOrtMeanSPTree::SingleTreeTraverser` + - Implements a depth-first single-tree traverser. + + * `NonOrtMeanSPTree::DualTreeTraverser` + - Implements a dual-depth-first dual-tree traverser. + +However, spill trees are primarily useful because the overlapping nodes allow +*defeatist* search to be effective. Defeatist search is non-backtracking: the +tree is traversed to one leaf only. For example, finding the approximate +nearest neighbor of a point `p` with defeatist search is done by recursing in +the tree, choosing the child with smallest minimum distance to `p`, and when a +leaf is encountered, choosing the closest point in the leaf to `p` as the +nearest neighbor. This is the strategy used in the +[original spill tree paper (pdf)](https://proceedings.neurips.cc/paper/2004/file/1102a326d5f7c9e04fc3c89d0ede88c9-Paper.pdf). + +Defeatist traversers, matching the API for a regular +[traversal](../../../developer/trees.md#traversals) are made available as the +following two classes: + + * `NonOrtMeanSPTree::DefeatistSingleTreeTraverser` + - Implements a depth-first single-tree defeatist traverser with no + backtracking. Traversal will terminate after the first leaf is visited. + + * `NonOrtMeanSPTree::DefeatistDualTreeTraverser` + - Implements a dual-depth-first dual-tree defeatist traversal with no + backtracking. For each query leaf node, traversal will terminate after the + first reference leaf node is visited. + +Any [`RuleType`](../../../developer/trees.md#rules) that is being used with a +defeatist traversal, in addition to the functions required by the `RuleType` +API, must implement the following functions: + +``` +// This is only required for single-tree defeatist traversals. +// It should return the index of the branch that should be chosen for the given +// query point and reference node. +template +size_t GetBestChild(const VecType& queryPoint, TreeType& referenceNode); + +// This is only required for dual-tree defeatist traversals. +// It should return the index of the best child of the reference node that +// should be chosen for the given query node. +template +size_t GetBestChild(TreeType& queryNode, TreeType& referenceNode); + +// Return the minimum number of base cases (point-to-point computations) that +// are required during the traversal. +size_t MinimumBaseCases(); +``` + +## Example usage + +Build an `NonOrtMeanSPTree` on the `cloud` dataset and print basic statistics +about the tree. + +```c++ +// See https://datasets.mlpack.org/cloud.csv. +arma::mat dataset; +mlpack::data::Load("cloud.csv", dataset, true); + +// Build the spill tree with a tau (margin) of 0.2 and a leaf size of 10. +// (This means that nodes are split until they contain 10 or fewer points.) +// +// The std::move() means that `dataset` will be empty after this call, and no +// data will be copied during tree building. +// +// When C++20 is enabled, then the <> is not necessary and the following line +// will work: +// mlpack::NonOrtMeanSPTree tree(std::move(dataset), 0.2, 10); +mlpack::NonOrtMeanSPTree<> tree(std::move(dataset), 0.2, 10); + +// Print the bounding ball of the root node. +std::cout << "Bounding ball of root node:" << std::endl; +std::cout << " Center: " << tree.Bound().Center().t(); +std::cout << " Radius: " << tree.Bound().Radius() << "." << std::endl; +std::cout << std::endl; + +// Print the number of descendant points of the root, and of each of its +// children. +std::cout << "Descendant points of root: " + << tree.NumDescendants() << "." << std::endl; +std::cout << "Descendant points of left child: " + << tree.Left()->NumDescendants() << "." << std::endl; +std::cout << "Descendant points of right child: " + << tree.Right()->NumDescendants() << "." << std::endl; +std::cout << std::endl; + +// Compute the center of the NonOrtMeanSPTree. THis is the same as the center +// of the bounding ball of the root. +arma::vec center; +tree.Center(center); +std::cout << "Center of tree: " << center.t(); +``` + +--- + +Build two `NonOrtMeanSPTree`s on subsets of the corel dataset and compute +minimum and maximum distances between different nodes in the tree. + +```c++ +// See https://datasets.mlpack.org/corel-histogram.csv. +arma::mat dataset; +mlpack::data::Load("corel-histogram.csv", dataset, true); + +// Build trees on the first half and the second half of points. Use a tau +// (overlap) parameter of 0.3, which is tuned to this dataset, and a rho value +// of 0.6 to prevent the trees getting too deep. +mlpack::NonOrtMeanSPTree<> tree1(dataset.cols(0, dataset.n_cols / 2), + 0.3, 20, 0.6); +mlpack::NonOrtMeanSPTree<> tree2(dataset.cols(dataset.n_cols / 2 + 1, + dataset.n_cols - 1), + 0.3, 20, 0.6); + +// Compute the maximum distance between the trees. +std::cout << "Maximum distance between tree root nodes: " + << tree1.MaxDistance(tree2) << "." << std::endl; + +// Get the leftmost grandchild of the first tree's root---if it exists. +if (!tree1.IsLeaf() && !tree1.Child(0).IsLeaf()) +{ + mlpack::NonOrtMeanSPTree<>& node1 = tree1.Child(0).Child(0); + + // Get the rightmost grandchild of the second tree's root---if it exists. + if (!tree2.IsLeaf() && !tree2.Child(1).IsLeaf()) + { + mlpack::NonOrtMeanSPTree<>& node2 = tree2.Child(1).Child(1); + + // Print the minimum and maximum distance between the nodes. + mlpack::Range dists = node1.RangeDistance(node2); + std::cout << "Possible distances between two grandchild nodes: [" + << dists.Lo() << ", " << dists.Hi() << "]." << std::endl; + + // Print the minimum distance between the first node and the first + // descendant point of the second node. + const size_t descendantIndex = node2.Descendant(0); + const double descendantMinDist = + node1.MinDistance(node2.Dataset().col(descendantIndex)); + std::cout << "Minimum distance between grandchild node and descendant " + << "point: " << descendantMinDist << "." << std::endl; + + // Which child of node2 is closer to node1? + const size_t closerIndex = node2.GetNearestChild(node1); + if (closerIndex == 0) + std::cout << "The left child of node2 is closer to node1." << std::endl; + else if (closerIndex == 1) + std::cout << "The right child of node2 is closer to node1." << std::endl; + else // closerIndex == 2 in this case. + std::cout << "Both children of node2 are equally close to node1." + << std::endl; + + // And which child of node1 is further from node2? + const size_t furtherIndex = node1.GetFurthestChild(node2); + if (furtherIndex == 0) + std::cout << "The left child of node1 is further from node2." + << std::endl; + else if (furtherIndex == 1) + std::cout << "The right child of node1 is further from node2." + << std::endl; + else // furtherIndex == 2 in this case. + std::cout << "Both children of node1 are equally far from node2." + << std::endl; + } +} +``` + +--- + +Build a `NonOrtMeanSPTree` on 32-bit floating point data and save it to disk. + +```c++ +// See https://datasets.mlpack.org/corel-histogram.csv. +arma::fmat dataset; +mlpack::data::Load("corel-histogram.csv", dataset); + +// Build the NonOrtMeanSPTree using 32-bit floating point data as the matrix +// type. We will still use the default EmptyStatistic and EuclideanDistance +// parameters. +mlpack::NonOrtSPTree tree(std::move(dataset), 0.1, 20, 0.6); + +// Save the tree to disk with the name 'tree'. +mlpack::data::Save("tree.bin", "tree", tree); + +std::cout << "Saved tree with " << tree.Dataset().n_cols << " points to " + << "'tree.bin'." << std::endl; +``` + +--- + +Load a 32-bit floating point `NonOrtMeanSPTree` from disk, then traverse it +manually and find the number of nodes whose children overlap. + +```c++ +// This assumes the tree has already been saved to 'tree.bin' (as in the example +// above). + +// This convenient typedef saves us a long type name! +using TreeType = mlpack::NonOrtMeanSPTree; + +TreeType tree; +mlpack::data::Load("tree.bin", "tree", tree); +std::cout << "Tree loaded with " << tree.NumDescendants() << " points." + << std::endl; + +// Recurse in a depth-first manner. Count both the total number of non-leaves, +// and the number of non-leaves that have overlapping children. +size_t overlapCount = 0; +size_t totalInternalNodeCount = 0; +std::stack stack; +stack.push(&tree); +while (!stack.empty()) +{ + TreeType* node = stack.top(); + stack.pop(); + + if (node->IsLeaf()) + continue; + + if (node->Overlap()) + ++overlapCount; + ++totalInternalNodeCount; + + stack.push(node->Left()); + stack.push(node->Right()); +} + +// Note that it would be possible to use TreeType::SingleTreeTraverser to +// perform the recursion above, but that is more well-suited for more complex +// tasks that require pruning and other non-trivial behavior; so using a simple +// stack is the better option here. + +// Print the results. +std::cout << overlapCount << " out of " << totalInternalNodeCount + << " internal nodes have overlapping children." << std::endl; +``` + +--- + +Use a defeatist traversal to find the approximate nearest neighbor of the third +and fourth points in the `corel-histogram` dataset. (Note: this can also be +done more easily with the `KNN` class! This example is a demonstration of how +to use the defeatist traverser.) + + + +For this example, we must first define a +[`RuleType` class](../../../developer/trees.md#rules). + +```c++ +// For simplicity, this only implements those methods required by single-tree +// traversals, and cannot be used with a dual-tree traversal. +// +// `.Reset()` must be called before any additional single-tree traversals after +// the first is run. +class SpillNearestNeighborRule +{ + public: + // Store the dataset internally. + SpillNearestNeighborRule(const arma::mat& dataset) : + dataset(dataset), + nearestNeighbor(size_t(-1)), + nearestDistance(DBL_MAX) { } + + // Compute the base case (point-to-point comparison). + double BaseCase(const size_t queryIndex, const size_t referenceIndex) + { + // Skip the base case if the points are the same. + if (queryIndex == referenceIndex) + return 0.0; + + const double dist = mlpack::EuclideanDistance::Evaluate( + dataset.col(queryIndex), dataset.col(referenceIndex)); + + if (dist < nearestDistance) + { + nearestNeighbor = referenceIndex; + nearestDistance = dist; + } + + return dist; + } + + // Score the given node in the tree; if it is sufficiently far away that it + // cannot contain a better nearest neighbor candidate, we can prune it. + template + double Score(const size_t queryIndex, const TreeType& referenceNode) const + { + const double minDist = referenceNode.MinDistance(dataset.col(queryIndex)); + if (minDist > nearestDistance) + return DBL_MAX; // Prune: this cannot contain a better candidate! + + return minDist; + } + + // Rescore the given node/point combination. Note that this will not be used + // by the defeatist traversal as it never backtracks, but we include it for + // completeness because the RuleType API requires it. + template + double Rescore(const size_t, const TreeType&, const double oldScore) const + { + if (oldScore > nearestDistance) + return DBL_MAX; // Prune: the node is too far away. + return oldScore; + } + + // This is required by defeatist traversals to select the best reference + // child to recurse into for overlapping nodes. + template + size_t GetBestChild(const size_t queryIndex, TreeType& referenceNode) + const + { + return referenceNode.GetNearestChild(dataset.col(queryIndex)); + } + + // We must perform at least two base cases in order to have a result. Note + // that this is two, and not one, because we skip base cases where the query + // and reference points are the same. That can only happen a maximum of once, + // so to ensure that we compare a query point to a different reference point + // at least once, we must return 2 here. + size_t MinimumBaseCases() const { return 2; } + + // Get the results (to be called after the traversal). + size_t NearestNeighbor() const { return nearestNeighbor; } + double NearestDistance() const { return nearestDistance; } + + // Reset the internal statistics for an additional traversal. + void Reset() + { + nearestNeighbor = size_t(-1); + nearestDistance = DBL_MAX; + } + + private: + const arma::mat& dataset; + + size_t nearestNeighbor; + double nearestDistance; +}; +``` + +```c++ +// See https://datasets.mlpack.org/corel-histogram.csv. +arma::mat dataset; +mlpack::data::Load("corel-histogram.csv", dataset, true); + +// Build two trees, one with a lot of overlap, and one with no overlap +// (e.g. tau = 0). +mlpack::NonOrtMeanSPTree<> tree1(dataset, 0.5, 10), tree2(dataset, 0.0, 10); + +// Construct the rule types, and then the traversals. +SpillNearestNeighborRule r1(dataset), r2(dataset); + +mlpack::NonOrtMeanSPTree<>::DefeatistSingleTreeTraverser< + SpillNearestNeighborRule> t1(r1), t2(r2); + +// Search for the approximate nearest neighbor of point 3 using both trees. +t1.Traverse(3, tree1); +t2.Traverse(3, tree2); + +std::cout << "Approximate nearest neighbor of point 3:" << std::endl; +std::cout << " - Non-axis-aligned mean-split spill tree with overlap 0.5 " + << "found: point " << r1.NearestNeighbor() << ", distance " + << r1.NearestDistance() << "." << std::endl; + +std::cout << " - Non-axis-aligned mean-split spill tree with no overlap " + << "found: point " << r2.NearestNeighbor() << ", distance " + << r2.NearestDistance() << "." << std::endl; + +// Now search for point 6. +r1.Reset(); +r2.Reset(); + +t1.Traverse(6, tree1); +t2.Traverse(6, tree2); + +std::cout << "Approximate nearest neighbor of point 6:" << std::endl; +std::cout << " - Non-axis-aligned mean-split spill tree with overlap 0.5 " + << "found: point " << r1.NearestNeighbor() << ", distance " + << r1.NearestDistance() << "." << std::endl; + +std::cout << " - Non-axis-aligned mean-split spill tree with no overlap " + << "found: point " << r2.NearestNeighbor() << ", distance " + << r2.NearestDistance() << "." << std::endl; +``` diff --git a/doc/user/core/trees/non_ort_sp_tree.md b/doc/user/core/trees/non_ort_sp_tree.md new file mode 100644 index 0000000000..f0dfc62334 --- /dev/null +++ b/doc/user/core/trees/non_ort_sp_tree.md @@ -0,0 +1,742 @@ +# `NonOrtSPTree` + +The `NonOrtSPTree` class implements the hybrid spill tree with +non-axis-orthogonal splitting hyperplanes; this is a binary space partitioning +tree that allows overlapping volumes between nodes. This type of tree can be +more effective than trees like the [`KDTree`](kdtree.md) for approximate nearest +neighbor search and related tasks. + +`NonOrtSPTree` supports three template parameters for configurable behavior, and +implements all the functionality required by the [TreeType +API](../../../developer/trees.md#the-treetype-api), plus some additional +functionality specific to spill trees. `NonOrtSPTree` is built on the more +generic [`SpillTree`](spill_tree.md) class, so if fully custom behavior is +desired, that + + * [Template parameters](#template-parameters) + * [Constructors](#constructors) + * [Basic tree properties](#basic-tree-properties) + * [Bounding distances with the tree](#bounding-distances-with-the-tree) + * [Tree traversals](#tree-traversals) + * [Example usage](#example-usage) + +## See also + + + + * [`SpillTree`](spill_tree.md) + * [`SPTree`](sp_tree.md) + * [`MeanSPTree`](mean_sp_tree.md) + * [`NonOrtMeanSPTree`](non_ort_mean_sp_tree.md) + * [`BinarySpaceTree`](binary_space_tree.md) + * [An Investigation of Practical Approximate Nearest Neighbor Algorithms (pdf)](https://proceedings.neurips.cc/paper/2004/file/1102a326d5f7c9e04fc3c89d0ede88c9-Paper.pdf) + * [Tree-Independent Dual-Tree Algorithms (pdf)](https://www.ratml.org/pub/pdf/2013tree.pdf) + +## Template parameters + +In accordance with the +[TreeType API](../../../developer/trees.md#template-parameters-required-by-the-treetype-policy) +(see also +[this more detailed section](../../../developer/trees.md#template-parameters)), +the `NonOrtSPTree` class takes three template parameters: + +``` +NonOrtSPTree +``` + + * `DistanceType`: the [distance metric](../distances.md) to use for distance + computations. + + * `StatisticType`: this holds auxiliary information in each tree node. By + default, [`EmptyStatistic`](binary_space_tree.md#emptystatistic) is used, + which holds no information. + - See the [`StatisticType`](binary_space_tree.md#statistictype) section in + the `BinarySpaceTree` documentation for more details. + + * `MatType`: the type of matrix used to represent points. Must be a type + matching the [Armadillo API](../../matrices.md). By default, `arma::mat` is + used, but other types such as `arma::fmat` or similar will work just fine. + +The `NonOrtSPTree` class itself is a convenience typedef of the generic +[`SpillTree`](spill_tree.md) class, using the +[`Hyperplane`](spill_tree.md#hyperplane) class as the splitting hyperplane type, +and the [`MidpointSpaceSplit`](spill_tree.md#midpointspacesplit) class as the +splitting strategy. + +If no template parameters are explicitly specified, then defaults are used: + +``` +NonOrtSPTree<> = NonOrtSPTree +``` + +## Constructors + +`NonOrtSPTree`s are constructed by iteratively finding splitting hyperplanes, +and points within a margin of the hyperplane are assigned to *both* child nodes. +Unlike the constructors of +[`BinarySpaceTree`](binary_space_tree.md#constructors), the dataset is not +permuted during construction. + +--- + + * `node = NonOrtSPTree(data, tau=0.0, maxLeafSize=20, rho=0.7)` + - Construct a `NonOrtSPTree` on the given `data`, using the specified + hyperparameters to control tree construction behavior. + - By default, a reference to `data` is stored. If `data` goes out of scope + after tree construction, memory errors will occur! To avoid this, either + pass the dataset or a copy with `std::move()` (e.g. `std::move(data)`); + when doing this, `data` will be set to an empty matrix. + +--- + + * `node = NonOrtSPTree(data, tau=0.0, maxLeafSize=20, rho=0.7)` + - Construct a `NonOrtSPTree` on the given `data`, using custom template + parameters, and using the specified hyperparameters to control tree + construction behavior. + - By default, a reference to `data` is stored. If `data` goes out of scope + after tree construction, memory errors will occur! To avoid this, either + pass the dataset or a copy with `std::move()` (e.g. `std::move(data)`); + when doing this, `data` will be set to an empty matrix. + +--- + + * `node = NonOrtSPTree()` + - Construct an empty `NonOrtSPTree` with no children, no points, and default + template parameters. + +--- + +***Notes:*** + + - The name `node` is used here for `NonOrtSPTree` objects instead of `tree`, + because each `NonOrtSPTree` object is a single node in the tree. The + constructor returns the node that is the root of the tree. + + - Inserting individual points or removing individual points from a + `NonOrtSPTree` is not supported, because this generally results in a tree + with very suboptimal hyperplane splits. It is better to simply build a new + `NonOrtSPTree` on the modified dataset. For trees that support individual + insertion and deletions, see the [`RectangleTree`](rectangle_tree.md) class + and all its variants (e.g. [`RTree`](r_tree.md), + [`RStarTree`](r_star_tree.md), etc.). + + - See also the + [developer documentation on tree constructors](../../../developer/trees.md#constructors-and-destructors). + +--- + +### Constructor parameters: + +| **name** | **type** | **description** | **default** | +|----------|----------|-----------------|-------------| +| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. | _(N/A)_ | +| `tau` | `double` | Width of spill margin: points within `tau` of the splitting hyperplane of a node will be contained in both left and right children. | `0.0` | +| `maxLeafSize` | `size_t` | Maximum number of points to store in each leaf. | `20` | +| `rho` | `double` | Balance threshold. When splitting, if either overlapping node would contain a fraction of more than `rho` of the points, a non-overlapping split is performed. Must be in the range `[0.0, 1.0)`. | `0.7` | + +***Caveats***: + + * `tau` must be manually tuned for the properties of each dataset; the default, + `0.0`, will never allow overlap between nodes (and thus the created tree will + essentially be a non-overlapping [`BinarySpaceTree`](binary_space_tree.md)). + + * If `tau` is set too large, nodes will overlap too much and search quality + will be degraded. + + * `rho` implicitly controls the depth of the tree by forcing very overlapping + children to be non-overlapping. As `rho` gets closer to `1`, more overlap is + allowed, which in turn makes the tree deeper. If `rho` is set to `0.5` or + less, then all splits will be non-overlapping (and the tree will essentially + be a [`BinarySpaceTree`](binary_space_tree.md)). + +## Basic tree properties + +Once an `NonOrtSPTree` object is constructed, various properties of the tree can +be accessed or inspected. Many of these functions are required by the +[TreeType API](../../../developer/trees.md#the-treetype-api). + +### Navigating the tree + + * `node.NumChildren()` returns the number of children in `node`. This is + either `2` if `node` has children, or `0` if `node` is a leaf. + + * `node.IsLeaf()` returns a `bool` indicating whether or not `node` is a leaf. + + * `node.Child(i)` returns a `NonOrtSPTree&` that is the `i`th child. + - `i` must be `0` or `1`. + - This function should only be called if `node.NumChildren()` is not `0` + (e.g. if `node` is not a leaf). Note that this returns a valid + `NonOrtSPTree&` that can itself be used just like the root node of the + tree! + - `node.Left()` and `node.Right()` are convenience functions specific to + `NonOrtSPTree` that will return `NonOrtSPTree*` (pointers) to the left and + right children, respectively, or `NULL` if `node` has no children. + + * `node.Parent()` will return an `NonOrtSPTree*` that points to the parent of + `node`, or `NULL` if `node` is the root of the `NonOrtSPTree`. + +--- + +### Accessing members of a tree + + * `node.Overlap()` will return a `bool` that is `true` if `node`'s children are + overlapping, and `false` otherwise. + + * `node.Hyperplane()` will return an [`Hyperplane`](spill_tree.md#hyperplane) + object that represents the splitting hyperplane of `node`. + - All points in `node.Left()` are to the left of `node.Hyperplane()` if + `node.Overlap()` is `false`; otherwise, all points in `node.Left()` are to + the left of `node.Hyperplane() + tau`. + - All points in `node.Right()` are to the right of `node.Hyperplane()` if + `node.Overlap()` is `false`; otherwise, all points in `node.Right()` are to + the right of `node.Hyperplane() - tau`. + + * `node.Bound()` will return a + [`const BallBound&`](binary_space_tree.md#ballbound) representing the + bounding box associated with `node`. + - If a [custom `DistanceType` and/or `MatType`](#template-parameters) are + specified, then a `const BallBound&` is returned. + * `ElemType` is the element type of the specified `MatType` (e.g. `double` + for `arma::mat`, `float` for `arma::fmat`, etc.). + + * `node.Stat()` will return a `StatisticType&` holding the statistics of the + node that were computed during tree construction. + + * `node.Distance()` will return a `DistanceType&` that can be used to make + distance computations. + +See also the +[developer documentation](../../../developer/trees.md#basic-tree-functionality) +for basic tree functionality in mlpack. + +--- + +### Accessing data held in a tree + + * `node.Dataset()` will return a `const MatType&` that is the dataset the + tree was built on. + + * `node.NumPoints()` returns a `size_t` indicating the number of points held + directly in `node`. + - If `node` is not a leaf, this will return `0`, as `NonOrtSPTree` only holds + points directly in its leaves. + - If `node` is a leaf, then the number of points will be less than or equal + to the `maxLeafSize` that was specified when the tree was constructed. + + * `node.Point(i)` returns a `size_t` indicating the index of the `i`'th point + in `node.Dataset()`. + - `i` must be in the range `[0, node.NumPoints() - 1]` (inclusive). + - `node` must be a leaf (as non-leaves do not hold any points). + - The `i`'th point in `node` can then be accessed as + `node.Dataset().col(node.Point(i))`. + - Accessing the actual `i`'th point itself can be done with, e.g., + `node.Dataset().col(node.Point(i))`. + + * `node.NumDescendants()` returns a `size_t` indicating the number of points + held in all descendant leaves of `node`. + - If `node` is the root of the tree, then `node.NumDescendants()` will be + equal to `node.Dataset().n_cols`. + + * `node.Descendant(i)` returns a `size_t` indicating the index of the `i`'th + descendant point in `node.Dataset()`. + - `i` must be in the range `[0, node.NumDescendants() - 1]` (inclusive). + - `node` does not need to be a leaf. + - The `i`'th descendant point in `node` can then be accessed as + `node.Dataset().col(node.Descendant(i))`. + - Accessing the actual `i`'th descendant itself can be done with, e.g., + `node.Dataset().col(node.Descendant(i))`. + +--- + +### Accessing computed bound quantities of a tree + +The following quantities are cached for each node in a `NonOrtSPTree`, and so +accessing them does not require any computation. In the documentation below, +`ElemType` is the element type of the given `MatType`; e.g., if `MatType` is +`arma::mat`, then `ElemType` is `double`. + + * `node.FurthestPointDistance()` returns an `ElemType` representing the + distance between the center of the bound of `node` and the furthest point + held by `node`. + - If `node` is not a leaf, this returns 0 (because `node` does not hold any + points). + + * `node.FurthestDescendantDistance()` returns an `ElemType` representing the + distance between the center of the bound of `node` and the furthest + descendant point held by `node`. + + * `node.MinimumBoundDistance()` returns an `ElemType` representing the minimum + possible distance from the center of the node to any edge of its bound. + + * `node.ParentDistance()` returns an `ElemType` representing the distance + between the center of the bound of `node` and the center of the bound of its + parent. + - If `node` is the root of the tree, `0` is returned. + +***Note:*** for more details on each bound quantity, see the [developer +documentation](../../../developer/trees.md#complex-tree-functionality-and-bounds) +on bound quantities for trees. + +--- + +### Other functionality + + * `node.Center(center)` computes the center of the bound of `node` and stores + it in `center`. + - `center` should be of type `arma::Col&`, where `ElemType` is the + element type of the specified `MatType`. + - `center` will be set to have size equivalent to the dimensionality of the + dataset held by `node`. + - This is equivalent to calling `node.Bound().Center(center)`. + + * A `NonOrtSPTree` can be serialized with + [`data::Save()` and `data::Load()`](../../load_save.md#mlpack-objects). + +## Bounding distances with the tree + +The primary use of trees in mlpack is bounding distances to points or other tree +nodes. The following functions can be used for these tasks. + + * `node.GetNearestChild(point)` + * `node.GetFurthestChild(point)` + - Return a `size_t` indicating the index of the child (`0` for left, `1` for + right) that is closest to (or furthest from) `point`, with respect + to the `MinDistance()` (or `MaxDistance()`) function. + - If there is a tie, `0` (the left child) is returned. + - If `node` is a leaf, `0` is returned. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + + * `node.GetNearestChild(other)` + * `node.GetFurthestChild(other)` + - Return a `size_t` indicating the index of the child (`0` for left, `1` for + right) that is closest to (or furthest from) the `NonOrtSPTree` node + `other`, with respect to the `MinDistance()` (or `MaxDistance()`) function. + - If there is a tie, `0` (the left child) is returned. + - If `node` is a leaf, `0` is returned. + +--- + + * `node.MinDistance(point)` + * `node.MinDistance(other)` + - Return a `double` indicating the minimum possible distance between `node` + and `point`, or the `NonOrtSPTree` node `other`. + - This is equivalent to the minimum possible distance between any point + contained in the bounding hyperrectangle of `node` and `point`, or between + any point contained in the bounding hyperrectangle of `node` and any point + contained in the bounding hyperrectangle of `other`. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + + * `node.MaxDistance(point)` + * `node.MaxDistance(other)` + - Return a `double` indicating the maximum possible distance between `node` + and `point`, or the `NonOrtSPTree` node `other`. + - This is equivalent to the maximum possible distance between any point + contained in the bounding hyperrectangle of `node` and `point`, or between + any point contained in the bounding hyperrectangle of `node` and any point + contained in the bounding hyperrectangle of `other`. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + + * `node.RangeDistance(point)` + * `node.RangeDistance(other)` + - Return a [`RangeType`](../math.md#range) whose lower bound is + `node.MinDistance(point)` or `node.MinDistance(other)`, and whose upper + bound is `node.MaxDistance(point)` or `node.MaxDistance(other)`. + - `ElemType` is the element type of `MatType`. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + +## Tree traversals + +Like every mlpack tree, the `NonOrtSPTree` class provides a [single-tree and +dual-tree traversal](../../../developer/trees.md#traversals) that can be paired +with a [`RuleType` class](../../../developer/trees.md#rules) to implement a +single-tree or dual-tree algorithm. + + * `NonOrtSPTree::SingleTreeTraverser` + - Implements a depth-first single-tree traverser. + + * `NonOrtSPTree::DualTreeTraverser` + - Implements a dual-depth-first dual-tree traverser. + +However, spill trees are primarily useful because the overlapping nodes allow +*defeatist* search to be effective. Defeatist search is non-backtracking: the +tree is traversed to one leaf only. For example, finding the approximate +nearest neighbor of a point `p` with defeatist search is done by recursing in +the tree, choosing the child with smallest minimum distance to `p`, and when a +leaf is encountered, choosing the closest point in the leaf to `p` as the +nearest neighbor. This is the strategy used in the +[original spill tree paper (pdf)](https://proceedings.neurips.cc/paper/2004/file/1102a326d5f7c9e04fc3c89d0ede88c9-Paper.pdf). + +Defeatist traversers, matching the API for a regular +[traversal](../../../developer/trees.md#traversals) are made available as the +following two classes: + + * `NonOrtSPTree::DefeatistSingleTreeTraverser` + - Implements a depth-first single-tree defeatist traverser with no + backtracking. Traversal will terminate after the first leaf is visited. + + * `NonOrtSPTree::DefeatistDualTreeTraverser` + - Implements a dual-depth-first dual-tree defeatist traversal with no + backtracking. For each query leaf node, traversal will terminate after the + first reference leaf node is visited. + +Any [`RuleType`](../../../developer/trees.md#rules) that is being used with a +defeatist traversal, in addition to the functions required by the `RuleType` +API, must implement the following functions: + +``` +// This is only required for single-tree defeatist traversals. +// It should return the index of the branch that should be chosen for the given +// query point and reference node. +template +size_t GetBestChild(const VecType& queryPoint, TreeType& referenceNode); + +// This is only required for dual-tree defeatist traversals. +// It should return the index of the best child of the reference node that +// should be chosen for the given query node. +template +size_t GetBestChild(TreeType& queryNode, TreeType& referenceNode); + +// Return the minimum number of base cases (point-to-point computations) that +// are required during the traversal. +size_t MinimumBaseCases(); +``` + +## Example usage + +Build an `NonOrtSPTree` on the `cloud` dataset and print basic statistics about +the tree. + +```c++ +// See https://datasets.mlpack.org/cloud.csv. +arma::mat dataset; +mlpack::data::Load("cloud.csv", dataset, true); + +// Build the spill tree with a tau (margin) of 0.2 and a leaf size of 10. +// (This means that nodes are split until they contain 10 or fewer points.) +// +// The std::move() means that `dataset` will be empty after this call, and no +// data will be copied during tree building. +// +// When C++20 is enabled, then the <> is not necessary and the following line +// will work: +// mlpack::NonOrtSPTree tree(std::move(dataset), 0.2, 10); +mlpack::NonOrtSPTree<> tree(std::move(dataset), 0.2, 10); + +// Print the bounding ball of the root node. +std::cout << "Bounding ball of root node:" << std::endl; +std::cout << " Center: " << tree.Bound().Center().t(); +std::cout << " Radius: " << tree.Bound().Radius() << "." << std::endl; +std::cout << std::endl; + +// Print the number of descendant points of the root, and of each of its +// children. +std::cout << "Descendant points of root: " + << tree.NumDescendants() << "." << std::endl; +std::cout << "Descendant points of left child: " + << tree.Left()->NumDescendants() << "." << std::endl; +std::cout << "Descendant points of right child: " + << tree.Right()->NumDescendants() << "." << std::endl; +std::cout << std::endl; + +// Compute the center of the NonOrtSPTree. THis is the same as the center of +// the bounding ball of the root. +arma::vec center; +tree.Center(center); +std::cout << "Center of tree: " << center.t(); +``` + +--- + +Build two `NonOrtSPTree`s on subsets of the corel dataset and compute minimum +and maximum distances between different nodes in the tree. + +```c++ +// See https://datasets.mlpack.org/corel-histogram.csv. +arma::mat dataset; +mlpack::data::Load("corel-histogram.csv", dataset, true); + +// Build trees on the first half and the second half of points. Use a tau +// (overlap) parameter of 0.3, which is tuned to this dataset, and a rho value +// of 0.6 to prevent the trees getting too deep. +mlpack::NonOrtSPTree<> tree1(dataset.cols(0, dataset.n_cols / 2), 0.3, 20, 0.6); +mlpack::NonOrtSPTree<> tree2(dataset.cols(dataset.n_cols / 2 + 1, + dataset.n_cols - 1), 0.3, 20, 0.6); + +// Compute the maximum distance between the trees. +std::cout << "Maximum distance between tree root nodes: " + << tree1.MaxDistance(tree2) << "." << std::endl; + +// Get the leftmost grandchild of the first tree's root---if it exists. +if (!tree1.IsLeaf() && !tree1.Child(0).IsLeaf()) +{ + mlpack::NonOrtSPTree<>& node1 = tree1.Child(0).Child(0); + + // Get the rightmost grandchild of the second tree's root---if it exists. + if (!tree2.IsLeaf() && !tree2.Child(1).IsLeaf()) + { + mlpack::NonOrtSPTree<>& node2 = tree2.Child(1).Child(1); + + // Print the minimum and maximum distance between the nodes. + mlpack::Range dists = node1.RangeDistance(node2); + std::cout << "Possible distances between two grandchild nodes: [" + << dists.Lo() << ", " << dists.Hi() << "]." << std::endl; + + // Print the minimum distance between the first node and the first + // descendant point of the second node. + const size_t descendantIndex = node2.Descendant(0); + const double descendantMinDist = + node1.MinDistance(node2.Dataset().col(descendantIndex)); + std::cout << "Minimum distance between grandchild node and descendant " + << "point: " << descendantMinDist << "." << std::endl; + + // Which child of node2 is closer to node1? + const size_t closerIndex = node2.GetNearestChild(node1); + if (closerIndex == 0) + std::cout << "The left child of node2 is closer to node1." << std::endl; + else if (closerIndex == 1) + std::cout << "The right child of node2 is closer to node1." << std::endl; + else // closerIndex == 2 in this case. + std::cout << "Both children of node2 are equally close to node1." + << std::endl; + + // And which child of node1 is further from node2? + const size_t furtherIndex = node1.GetFurthestChild(node2); + if (furtherIndex == 0) + std::cout << "The left child of node1 is further from node2." + << std::endl; + else if (furtherIndex == 1) + std::cout << "The right child of node1 is further from node2." + << std::endl; + else // furtherIndex == 2 in this case. + std::cout << "Both children of node1 are equally far from node2." + << std::endl; + } +} +``` + +--- + +Build a `NonOrtSPTree` on 32-bit floating point data and save it to disk. + +```c++ +// See https://datasets.mlpack.org/corel-histogram.csv. +arma::fmat dataset; +mlpack::data::Load("corel-histogram.csv", dataset); + +// Build the NonOrtSPTree using 32-bit floating point data as the matrix type. +// We will still use the default EmptyStatistic and EuclideanDistance +// parameters. +mlpack::NonOrtSPTree tree(std::move(dataset), 0.1, 20, 0.6); + +// Save the tree to disk with the name 'tree'. +mlpack::data::Save("tree.bin", "tree", tree); + +std::cout << "Saved tree with " << tree.Dataset().n_cols << " points to " + << "'tree.bin'." << std::endl; +``` + +--- + +Load a 32-bit floating point `NonOrtSPTree` from disk, then traverse it manually +and find the number of nodes whose children overlap. + +```c++ +// This assumes the tree has already been saved to 'tree.bin' (as in the example +// above). + +// This convenient typedef saves us a long type name! +using TreeType = mlpack::NonOrtSPTree; + +TreeType tree; +mlpack::data::Load("tree.bin", "tree", tree); +std::cout << "Tree loaded with " << tree.NumDescendants() << " points." + << std::endl; + +// Recurse in a depth-first manner. Count both the total number of non-leaves, +// and the number of non-leaves that have overlapping children. +size_t overlapCount = 0; +size_t totalInternalNodeCount = 0; +std::stack stack; +stack.push(&tree); +while (!stack.empty()) +{ + TreeType* node = stack.top(); + stack.pop(); + + if (node->IsLeaf()) + continue; + + if (node->Overlap()) + ++overlapCount; + ++totalInternalNodeCount; + + stack.push(node->Left()); + stack.push(node->Right()); +} + +// Note that it would be possible to use TreeType::SingleTreeTraverser to +// perform the recursion above, but that is more well-suited for more complex +// tasks that require pruning and other non-trivial behavior; so using a simple +// stack is the better option here. + +// Print the results. +std::cout << overlapCount << " out of " << totalInternalNodeCount + << " internal nodes have overlapping children." << std::endl; +``` + +--- + +Use a defeatist traversal to find the approximate nearest neighbor of the third +and fourth points in the `corel-histogram` dataset. (Note: this can also be +done more easily with the `KNN` class! This example is a demonstration of how +to use the defeatist traverser.) + + + +For this example, we must first define a +[`RuleType` class](../../../developer/trees.md#rules). + +```c++ +// For simplicity, this only implements those methods required by single-tree +// traversals, and cannot be used with a dual-tree traversal. +// +// `.Reset()` must be called before any additional single-tree traversals after +// the first is run. +class SpillNearestNeighborRule +{ + public: + // Store the dataset internally. + SpillNearestNeighborRule(const arma::mat& dataset) : + dataset(dataset), + nearestNeighbor(size_t(-1)), + nearestDistance(DBL_MAX) { } + + // Compute the base case (point-to-point comparison). + double BaseCase(const size_t queryIndex, const size_t referenceIndex) + { + // Skip the base case if the points are the same. + if (queryIndex == referenceIndex) + return 0.0; + + const double dist = mlpack::EuclideanDistance::Evaluate( + dataset.col(queryIndex), dataset.col(referenceIndex)); + + if (dist < nearestDistance) + { + nearestNeighbor = referenceIndex; + nearestDistance = dist; + } + + return dist; + } + + // Score the given node in the tree; if it is sufficiently far away that it + // cannot contain a better nearest neighbor candidate, we can prune it. + template + double Score(const size_t queryIndex, const TreeType& referenceNode) const + { + const double minDist = referenceNode.MinDistance(dataset.col(queryIndex)); + if (minDist > nearestDistance) + return DBL_MAX; // Prune: this cannot contain a better candidate! + + return minDist; + } + + // Rescore the given node/point combination. Note that this will not be used + // by the defeatist traversal as it never backtracks, but we include it for + // completeness because the RuleType API requires it. + template + double Rescore(const size_t, const TreeType&, const double oldScore) const + { + if (oldScore > nearestDistance) + return DBL_MAX; // Prune: the node is too far away. + return oldScore; + } + + // This is required by defeatist traversals to select the best reference + // child to recurse into for overlapping nodes. + template + size_t GetBestChild(const size_t queryIndex, TreeType& referenceNode) + const + { + return referenceNode.GetNearestChild(dataset.col(queryIndex)); + } + + // We must perform at least two base cases in order to have a result. Note + // that this is two, and not one, because we skip base cases where the query + // and reference points are the same. That can only happen a maximum of once, + // so to ensure that we compare a query point to a different reference point + // at least once, we must return 2 here. + size_t MinimumBaseCases() const { return 2; } + + // Get the results (to be called after the traversal). + size_t NearestNeighbor() const { return nearestNeighbor; } + double NearestDistance() const { return nearestDistance; } + + // Reset the internal statistics for an additional traversal. + void Reset() + { + nearestNeighbor = size_t(-1); + nearestDistance = DBL_MAX; + } + + private: + const arma::mat& dataset; + + size_t nearestNeighbor; + double nearestDistance; +}; +``` + +```c++ +// See https://datasets.mlpack.org/corel-histogram.csv. +arma::mat dataset; +mlpack::data::Load("corel-histogram.csv", dataset, true); + +// Build two trees, one with a lot of overlap, and one with no overlap +// (e.g. tau = 0). +mlpack::NonOrtSPTree<> tree1(dataset, 0.5, 10), tree2(dataset, 0.0, 10); + +// Construct the rule types, and then the traversals. +SpillNearestNeighborRule r1(dataset), r2(dataset); + +mlpack::NonOrtSPTree<>::DefeatistSingleTreeTraverser + t1(r1), t2(r2); + +// Search for the approximate nearest neighbor of point 3 using both trees. +t1.Traverse(3, tree1); +t2.Traverse(3, tree2); + +std::cout << "Approximate nearest neighbor of point 3:" << std::endl; +std::cout << " - Non-axis-aligned spill tree with overlap 0.5 found: point " + << r1.NearestNeighbor() << ", distance " << r1.NearestDistance() + << "." << std::endl; + +std::cout << " - Non-axis-aligned spill tree with no overlap found: point " + << r2.NearestNeighbor() << ", distance " << r2.NearestDistance() + << "." << std::endl; + +// Now search for point 6. +r1.Reset(); +r2.Reset(); + +t1.Traverse(6, tree1); +t2.Traverse(6, tree2); + +std::cout << "Approximate nearest neighbor of point 6:" << std::endl; +std::cout << " - Non-axis-aligned spill tree with overlap 0.5 found: point " + << r1.NearestNeighbor() << ", distance " << r1.NearestDistance() + << "." << std::endl; + +std::cout << " - Non-axis-aligned spill tree with no overlap found: point " + << r2.NearestNeighbor() << ", distance " << r2.NearestDistance() + << "." << std::endl; +``` diff --git a/doc/user/core/trees/octree.md b/doc/user/core/trees/octree.md index 1856e907ae..1ac4bf4c83 100644 --- a/doc/user/core/trees/octree.md +++ b/doc/user/core/trees/octree.md @@ -131,13 +131,12 @@ different. not supported, because this generally results in a octree with very loose bounding boxes. It is better to simply build a new `Octree` on the modified dataset. For trees that support individual insertion and deletions, see the - `RectangleTree` class and all its variants (e.g. `RTree`, `RStarTree`, etc.). + [`RectangleTree`](rectangle_tree.md) class and all its variants (e.g. + [`RTree`](r_tree.md), [`RStarTree`](r_star_tree.md), etc.). - See also the [developer documentation on tree constructors](../../../developer/trees.md#constructors-and-destructors). - - --- ### Constructor parameters: diff --git a/doc/user/core/trees/r_plus_plus_tree.md b/doc/user/core/trees/r_plus_plus_tree.md index 7760cccc74..a6dc3bd4c7 100644 --- a/doc/user/core/trees/r_plus_plus_tree.md +++ b/doc/user/core/trees/r_plus_plus_tree.md @@ -152,7 +152,7 @@ The dataset is not permuted during the construction process. | **name** | **type** | **description** | **default** | |----------|----------|-----------------|-------------| -| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. Pass with `std::move(data)` to avoid copying the matrix. | _(N/A)_ | +| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. | _(N/A)_ | | `maxLeafSize` | `size_t` | Maximum number of points to store in each leaf. | `20` | | `minLeafSize` | `size_t` | Minimum number of points to store in each leaf. | `8` | | `maxNumChildren` | `size_t` | Maximum number of children allowed in each non-leaf node. | `5` | diff --git a/doc/user/core/trees/r_plus_tree.md b/doc/user/core/trees/r_plus_tree.md index 4cc86fd58c..3f465902b6 100644 --- a/doc/user/core/trees/r_plus_tree.md +++ b/doc/user/core/trees/r_plus_tree.md @@ -148,7 +148,7 @@ The dataset is not permuted during the construction process. | **name** | **type** | **description** | **default** | |----------|----------|-----------------|-------------| -| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. Pass with `std::move(data)` to avoid copying the matrix. | _(N/A)_ | +| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. | _(N/A)_ | | `maxLeafSize` | `size_t` | Maximum number of points to store in each leaf. | `20` | | `minLeafSize` | `size_t` | Minimum number of points to store in each leaf. | `8` | | `maxNumChildren` | `size_t` | Maximum number of children allowed in each non-leaf node. | `5` | diff --git a/doc/user/core/trees/r_star_tree.md b/doc/user/core/trees/r_star_tree.md index e84bd20a9f..bf6002d259 100644 --- a/doc/user/core/trees/r_star_tree.md +++ b/doc/user/core/trees/r_star_tree.md @@ -151,7 +151,7 @@ The dataset is not permuted during the construction process. | **name** | **type** | **description** | **default** | |----------|----------|-----------------|-------------| -| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. Pass with `std::move(data)` to avoid copying the matrix. | _(N/A)_ | +| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. | _(N/A)_ | | `maxLeafSize` | `size_t` | Maximum number of points to store in each leaf. | `20` | | `minLeafSize` | `size_t` | Minimum number of points to store in each leaf. | `8` | | `maxNumChildren` | `size_t` | Maximum number of children allowed in each non-leaf node. | `5` | diff --git a/doc/user/core/trees/r_tree.md b/doc/user/core/trees/r_tree.md index 0ff376b222..afad716f70 100644 --- a/doc/user/core/trees/r_tree.md +++ b/doc/user/core/trees/r_tree.md @@ -145,7 +145,7 @@ The dataset is not permuted during the construction process. | **name** | **type** | **description** | **default** | |----------|----------|-----------------|-------------| -| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. Pass with `std::move(data)` to avoid copying the matrix. | _(N/A)_ | +| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. | _(N/A)_ | | `maxLeafSize` | `size_t` | Maximum number of points to store in each leaf. | `20` | | `minLeafSize` | `size_t` | Minimum number of points to store in each leaf. | `8` | | `maxNumChildren` | `size_t` | Maximum number of children allowed in each non-leaf node. | `5` | diff --git a/doc/user/core/trees/rectangle_tree.md b/doc/user/core/trees/rectangle_tree.md index 882290dda3..df08fbe5d8 100644 --- a/doc/user/core/trees/rectangle_tree.md +++ b/doc/user/core/trees/rectangle_tree.md @@ -183,15 +183,13 @@ The dataset is not permuted during the construction process. - See also the [developer documentation on tree constructors](../../../developer/trees.md#constructors-and-destructors). - - --- ### Constructor parameters: | **name** | **type** | **description** | **default** | |----------|----------|-----------------|-------------| -| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. Pass with `std::move(data)` to avoid copying the matrix. | _(N/A)_ | +| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. | _(N/A)_ | | `maxLeafSize` | `size_t` | Maximum number of points to store in each leaf. | `20` | | `minLeafSize` | `size_t` | Minimum number of points to store in each leaf. | `8` | | `maxNumChildren` | `size_t` | Maximum number of children allowed in each non-leaf node. | `5` | diff --git a/doc/user/core/trees/rp_tree.md b/doc/user/core/trees/rp_tree.md index 557e14060b..40ac4c72ff 100644 --- a/doc/user/core/trees/rp_tree.md +++ b/doc/user/core/trees/rp_tree.md @@ -9,10 +9,9 @@ nearest neighbor search) in low dimensions---typically less than 100. An `RPTree` (or the similar [`MaxRPTree`](max_rp_tree.md)) may be preferred over a [`KDTree`](kdtree.md) or other tree structures as it is theoretically known to -adapt to the intrinsic dimension of the data. This is similar to the cover -tree, but the implementation is far simpler and as a result, more efficient. - - +adapt to the intrinsic dimension of the data. This is similar to the +[cover tree](cover_tree.md), but the implementation is far simpler and as a +result, more efficient. mlpack's `RPTree` implementation supports three template parameters for configurable behavior, and implements all the functionality required by the @@ -137,13 +136,11 @@ different. with very loose bounding boxes. It is better to simply build a new `RPTree` on the modified dataset. For trees that support individual insertion and deletions, see the [`RectangleTree`](rectangle_tree.md) class and all its - variants (e.g. [`RTree`](r_tree.md), `RStarTree`, etc.). + variants (e.g. [`RTree`](r_tree.md), [`RStarTree`](r_star_tree.md), etc.). - See also the [developer documentation on tree constructors](../../../developer/trees.md#constructors-and-destructors). - - --- ### Constructor parameters: @@ -432,7 +429,7 @@ mlpack::data::Load("cloud.csv", dataset, true); // // Note that the '<>' isn't necessary if C++20 is being used (e.g. // `mlpack::RPTree tree(...)` will work fine in C++20 or newer). -mlpack::RPTree<> tree(std::move(dataset)); +mlpack::RPTree<> tree(std::move(dataset), 10); // Print the bounding box of the root node. std::cout << "Bounding box of root node:" << std::endl; diff --git a/doc/user/core/trees/sp_tree.md b/doc/user/core/trees/sp_tree.md new file mode 100644 index 0000000000..d02cf44a84 --- /dev/null +++ b/doc/user/core/trees/sp_tree.md @@ -0,0 +1,749 @@ +# `SPTree` + +The `SPTree` class implements the standard hybrid spill tree, a binary space +partitioning tree that allows overlapping volumes between nodes. This type of +tree can be more effective than trees like the [`KDTree`](kdtree.md) for +approximate nearest neighbor search and related tasks. + +`SPTree` supports three template parameters for configurable behavior, and +implements all the functionality required by the [TreeType +API](../../../developer/trees.md#the-treetype-api), plus some additional +functionality specific to spill trees. `SPTree` is built on the more generic +[`SpillTree`](spill_tree.md) class, so if fully custom behavior is desired, that + + * [Template parameters](#template-parameters) + * [Constructors](#constructors) + * [Basic tree properties](#basic-tree-properties) + * [Bounding distances with the tree](#bounding-distances-with-the-tree) + * [Tree traversals](#tree-traversals) + * [Example usage](#example-usage) + +## See also + + + + * [`SpillTree`](spill_tree.md) + * [`MeanSPTree`](mean_sp_tree.md) + * [`NonOrtSPTree`](non_ort_sp_tree.md) + * [`NonOrtMeanSPTree`](non_ort_mean_sp_tree.md) + * [`BinarySpaceTree`](binary_space_tree.md) + * [An Investigation of Practical Approximate Nearest Neighbor Algorithms (pdf)](https://proceedings.neurips.cc/paper/2004/file/1102a326d5f7c9e04fc3c89d0ede88c9-Paper.pdf) + * [Tree-Independent Dual-Tree Algorithms (pdf)](https://www.ratml.org/pub/pdf/2013tree.pdf) + +## Template parameters + +In accordance with the +[TreeType API](../../../developer/trees.md#template-parameters-required-by-the-treetype-policy) +(see also +[this more detailed section](../../../developer/trees.md#template-parameters)), +the `SPTree` class takes three template parameters: + +``` +SPTree +``` + + * `DistanceType`: the [distance metric](../distances.md) to use for distance + computations. Because the `SPTree` internally uses + [`HRectBound`](binary_space_tree.md#hrectbound), this is required to be + [`EuclideanDistance`](../distances.md#lmetric). See + [`NonOrtSPTree`](non_ort_sp_tree.md) for a version of the spill tree where + arbitrary distance metrics are allowed. + + * `StatisticType`: this holds auxiliary information in each tree node. By + default, [`EmptyStatistic`](binary_space_tree.md#emptystatistic) is used, + which holds no information. + - See the [`StatisticType`](binary_space_tree.md#statistictype) section in + the `BinarySpaceTree` documentation for more details. + + * `MatType`: the type of matrix used to represent points. Must be a type + matching the [Armadillo API](../../matrices.md). By default, `arma::mat` is + used, but other types such as `arma::fmat` or similar will work just fine. + +The `SPTree` class itself is a convenience typedef of the generic +[`SpillTree`](spill_tree.md) class, using the +[`AxisOrthogonalHyperplane`](spill_tree.md#axisorthogonalhyperplane) class as +the splitting hyperplane type, and the +[`MidpointSpaceSplit`](spill_tree.md#midpointspacesplit) class as the splitting +strategy. + +If no template parameters are explicitly specified, then defaults are used: + +``` +SPTree<> = SPTree +``` + +## Constructors + +`SPTree`s are constructed by iteratively finding splitting hyperplanes, and +points within a margin of the hyperplane are assigned to *both* child nodes. +Unlike the constructors of +[`BinarySpaceTree`](binary_space_tree.md#constructors), the dataset is not +permuted during construction. + +--- + + * `node = SPTree(data, tau=0.0, maxLeafSize=20, rho=0.7)` + - Construct an `SPTree` on the given `data`, using the specified + hyperparameters to control tree construction behavior. + - By default, a reference to `data` is stored. If `data` goes out of scope + after tree construction, memory errors will occur! To avoid this, either + pass the dataset or a copy with `std::move()` (e.g. `std::move(data)`); + when doing this, `data` will be set to an empty matrix. + +--- + + * `node = SPTree(data, tau=0.0, maxLeafSize=20, rho=0.7)` + - Construct an `SPTree` on the given `data`, using custom template + parameters, and using the specified hyperparameters to control tree + construction behavior. + - By default, a reference to `data` is stored. If `data` goes out of scope + after tree construction, memory errors will occur! To avoid this, either + pass the dataset or a copy with `std::move()` (e.g. `std::move(data)`); + when doing this, `data` will be set to an empty matrix. + +--- + + * `node = SPTree()` + - Construct an empty `SPTree` with no children, no points, and default + template parameters. + +--- + +***Notes:*** + + - The name `node` is used here for `SPTree` objects instead of `tree`, because + each `SPTree` object is a single node in the tree. The constructor returns + the node that is the root of the tree. + + - Inserting individual points or removing individual points from an `SPTree` is + not supported, because this generally results in a tree with very suboptimal + hyperplane splits. It is better to simply build a new `SPTree` on the + modified dataset. For trees that support individual insertion and deletions, + see the [`RectangleTree`](rectangle_tree.md) class and all its variants (e.g. + [`RTree`](r_tree.md), [`RStarTree`](r_star_tree.md), etc.). + + - See also the + [developer documentation on tree constructors](../../../developer/trees.md#constructors-and-destructors). + +--- + +### Constructor parameters: + +| **name** | **type** | **description** | **default** | +|----------|----------|-----------------|-------------| +| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. | _(N/A)_ | +| `tau` | `double` | Width of spill margin: points within `tau` of the splitting hyperplane of a node will be contained in both left and right children. | `0.0` | +| `maxLeafSize` | `size_t` | Maximum number of points to store in each leaf. | `20` | +| `rho` | `double` | Balance threshold. When splitting, if either overlapping node would contain a fraction of more than `rho` of the points, a non-overlapping split is performed. Must be in the range `[0.0, 1.0)`. | `0.7` | + +***Caveats***: + + * `tau` must be manually tuned for the properties of each dataset; the default, + `0.0`, will never allow overlap between nodes (and thus the created tree will + essentially be a non-overlapping [`BinarySpaceTree`](binary_space_tree.md)). + + * If `tau` is set too large, nodes will overlap too much and search quality + will be degraded. + + * `rho` implicitly controls the depth of the tree by forcing very overlapping + children to be non-overlapping. As `rho` gets closer to `1`, more overlap is + allowed, which in turn makes the tree deeper. If `rho` is set to `0.5` or + less, then all splits will be non-overlapping (and the tree will essentially + be a [`BinarySpaceTree`](binary_space_tree.md)). + +## Basic tree properties + +Once an `SPTree` object is constructed, various properties of the tree can be +accessed or inspected. Many of these functions are required by the +[TreeType API](../../../developer/trees.md#the-treetype-api). + +### Navigating the tree + + * `node.NumChildren()` returns the number of children in `node`. This is + either `2` if `node` has children, or `0` if `node` is a leaf. + + * `node.IsLeaf()` returns a `bool` indicating whether or not `node` is a leaf. + + * `node.Child(i)` returns an `SPTree&` that is the `i`th child. + - `i` must be `0` or `1`. + - This function should only be called if `node.NumChildren()` is not `0` + (e.g. if `node` is not a leaf). Note that this returns a valid + `SPTree&` that can itself be used just like the root node of the + tree! + - `node.Left()` and `node.Right()` are convenience functions specific to + `SPTree` that will return `SPTree*` (pointers) to the left and right + children, respectively, or `NULL` if `node` has no children. + + * `node.Parent()` will return an `SPTree*` that points to the parent of + `node`, or `NULL` if `node` is the root of the `SPTree`. + +--- + +### Accessing members of a tree + + * `node.Overlap()` will return a `bool` that is `true` if `node`'s children are + overlapping, and `false` otherwise. + + * `node.Hyperplane()` will return an + [`AxisOrthogonalHyperplane`](spill_tree.md#axisorthogonalhyperplane) object + that represents the axis-aligned splitting hyperplane of `node`. + - All points in `node.Left()` are to the left of `node.Hyperplane()` if + `node.Overlap()` is `false`; otherwise, all points in `node.Left()` are to + the left of `node.Hyperplane() + tau`. + - All points in `node.Right()` are to the right of `node.Hyperplane()` if + `node.Overlap()` is `false`; otherwise, all points in `node.Right()` are to + the right of `node.Hyperplane() - tau`. + + * `node.Bound()` will return a + [`const HRectBound&`](binary_space_tree.md#hrectbound) representing the + bounding box associated with `node`. + - If a [custom `DistanceType` and/or `MatType`](#template-parameters) are + specified, then a `const HRectBound&` is returned. + * `ElemType` is the element type of the specified `MatType` (e.g. `double` + for `arma::mat`, `float` for `arma::fmat`, etc.). + + * `node.Stat()` will return a `StatisticType&` holding the statistics of the + node that were computed during tree construction. + + * `node.Distance()` will return a `EuclideanDistance&`. Because + `EuclideanDistance` has no instantiated members, this is unlikely to be + useful, but is required to satisfy the + [`TreeType` API](../../../developer/trees.md#the-treetype-api). + +See also the +[developer documentation](../../../developer/trees.md#basic-tree-functionality) +for basic tree functionality in mlpack. + +--- + +### Accessing data held in a tree + + * `node.Dataset()` will return a `const MatType&` that is the dataset the + tree was built on. + + * `node.NumPoints()` returns a `size_t` indicating the number of points held + directly in `node`. + - If `node` is not a leaf, this will return `0`, as `SPTree` only holds + points directly in its leaves. + - If `node` is a leaf, then the number of points will be less than or equal + to the `maxLeafSize` that was specified when the tree was constructed. + + * `node.Point(i)` returns a `size_t` indicating the index of the `i`'th point + in `node.Dataset()`. + - `i` must be in the range `[0, node.NumPoints() - 1]` (inclusive). + - `node` must be a leaf (as non-leaves do not hold any points). + - The `i`'th point in `node` can then be accessed as + `node.Dataset().col(node.Point(i))`. + - Accessing the actual `i`'th point itself can be done with, e.g., + `node.Dataset().col(node.Point(i))`. + + * `node.NumDescendants()` returns a `size_t` indicating the number of points + held in all descendant leaves of `node`. + - If `node` is the root of the tree, then `node.NumDescendants()` will be + equal to `node.Dataset().n_cols`. + + * `node.Descendant(i)` returns a `size_t` indicating the index of the `i`'th + descendant point in `node.Dataset()`. + - `i` must be in the range `[0, node.NumDescendants() - 1]` (inclusive). + - `node` does not need to be a leaf. + - The `i`'th descendant point in `node` can then be accessed as + `node.Dataset().col(node.Descendant(i))`. + - Accessing the actual `i`'th descendant itself can be done with, e.g., + `node.Dataset().col(node.Descendant(i))`. + +--- + +### Accessing computed bound quantities of a tree + +The following quantities are cached for each node in an `SPTree`, and so +accessing them does not require any computation. In the documentation below, +`ElemType` is the element type of the given `MatType`; e.g., if `MatType` is +`arma::mat`, then `ElemType` is `double`. + + * `node.FurthestPointDistance()` returns an `ElemType` representing the + distance between the center of the bound of `node` and the furthest point + held by `node`. + - If `node` is not a leaf, this returns 0 (because `node` does not hold any + points). + + * `node.FurthestDescendantDistance()` returns an `ElemType` representing the + distance between the center of the bound of `node` and the furthest + descendant point held by `node`. + + * `node.MinimumBoundDistance()` returns an `ElemType` representing the minimum + possible distance from the center of the node to any edge of its bound. + + * `node.ParentDistance()` returns an `ElemType` representing the distance + between the center of the bound of `node` and the center of the bound of its + parent. + - If `node` is the root of the tree, `0` is returned. + +***Note:*** for more details on each bound quantity, see the [developer +documentation](../../../developer/trees.md#complex-tree-functionality-and-bounds) +on bound quantities for trees. + +--- + +### Other functionality + + * `node.Center(center)` computes the center of the bound of `node` and stores + it in `center`. + - `center` should be of type `arma::Col&`, where `ElemType` is the + element type of the specified `MatType`. + - `center` will be set to have size equivalent to the dimensionality of the + dataset held by `node`. + - This is equivalent to calling `node.Bound().Center(center)`. + + * An `SPTree` can be serialized with + [`data::Save()` and `data::Load()`](../../load_save.md#mlpack-objects). + +## Bounding distances with the tree + +The primary use of trees in mlpack is bounding distances to points or other tree +nodes. The following functions can be used for these tasks. + + * `node.GetNearestChild(point)` + * `node.GetFurthestChild(point)` + - Return a `size_t` indicating the index of the child (`0` for left, `1` for + right) that is closest to (or furthest from) `point`, with respect + to the `MinDistance()` (or `MaxDistance()`) function. + - If there is a tie, `0` (the left child) is returned. + - If `node` is a leaf, `0` is returned. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + + * `node.GetNearestChild(other)` + * `node.GetFurthestChild(other)` + - Return a `size_t` indicating the index of the child (`0` for left, `1` for + right) that is closest to (or furthest from) the `SPTree` node `other`, + with respect to the `MinDistance()` (or `MaxDistance()`) function. + - If there is a tie, `0` (the left child) is returned. + - If `node` is a leaf, `0` is returned. + +--- + + * `node.MinDistance(point)` + * `node.MinDistance(other)` + - Return a `double` indicating the minimum possible distance between `node` + and `point`, or the `SPTree` node `other`. + - This is equivalent to the minimum possible distance between any point + contained in the bounding hyperrectangle of `node` and `point`, or between + any point contained in the bounding hyperrectangle of `node` and any point + contained in the bounding hyperrectangle of `other`. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + + * `node.MaxDistance(point)` + * `node.MaxDistance(other)` + - Return a `double` indicating the maximum possible distance between `node` + and `point`, or the `SPTree` node `other`. + - This is equivalent to the maximum possible distance between any point + contained in the bounding hyperrectangle of `node` and `point`, or between + any point contained in the bounding hyperrectangle of `node` and any point + contained in the bounding hyperrectangle of `other`. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + + * `node.RangeDistance(point)` + * `node.RangeDistance(other)` + - Return a [`RangeType`](../math.md#range) whose lower bound is + `node.MinDistance(point)` or `node.MinDistance(other)`, and whose upper + bound is `node.MaxDistance(point)` or `node.MaxDistance(other)`. + - `ElemType` is the element type of `MatType`. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + +## Tree traversals + +Like every mlpack tree, the `SPTree` class provides a [single-tree and +dual-tree traversal](../../../developer/trees.md#traversals) that can be paired +with a [`RuleType` class](../../../developer/trees.md#rules) to implement a +single-tree or dual-tree algorithm. + + * `SPTree::SingleTreeTraverser` + - Implements a depth-first single-tree traverser. + + * `SPTree::DualTreeTraverser` + - Implements a dual-depth-first dual-tree traverser. + +However, spill trees are primarily useful because the overlapping nodes allow +*defeatist* search to be effective. Defeatist search is non-backtracking: the +tree is traversed to one leaf only. For example, finding the approximate +nearest neighbor of a point `p` with defeatist search is done by recursing in +the tree, choosing the child with smallest minimum distance to `p`, and when a +leaf is encountered, choosing the closest point in the leaf to `p` as the +nearest neighbor. This is the strategy used in the +[original spill tree paper (pdf)](https://proceedings.neurips.cc/paper/2004/file/1102a326d5f7c9e04fc3c89d0ede88c9-Paper.pdf). + +Defeatist traversers, matching the API for a regular +[traversal](../../../developer/trees.md#traversals) are made available as the +following two classes: + + * `SPTree::DefeatistSingleTreeTraverser` + - Implements a depth-first single-tree defeatist traverser with no + backtracking. Traversal will terminate after the first leaf is visited. + + * `SPTree::DefeatistDualTreeTraverser` + - Implements a dual-depth-first dual-tree defeatist traversal with no + backtracking. For each query leaf node, traversal will terminate after the + first reference leaf node is visited. + +Any [`RuleType`](../../../developer/trees.md#rules) that is being used with a +defeatist traversal, in addition to the functions required by the `RuleType` +API, must implement the following functions: + +``` +// This is only required for single-tree defeatist traversals. +// It should return the index of the branch that should be chosen for the given +// query point and reference node. +template +size_t GetBestChild(const VecType& queryPoint, TreeType& referenceNode); + +// This is only required for dual-tree defeatist traversals. +// It should return the index of the best child of the reference node that +// should be chosen for the given query node. +template +size_t GetBestChild(TreeType& queryNode, TreeType& referenceNode); + +// Return the minimum number of base cases (point-to-point computations) that +// are required during the traversal. +size_t MinimumBaseCases(); +``` + +## Example usage + +Build an `SPTree` on the `cloud` dataset and print basic statistics about the +tree. + +```c++ +// See https://datasets.mlpack.org/cloud.csv. +arma::mat dataset; +mlpack::data::Load("cloud.csv", dataset, true); + +// Build the spill tree with a tau (margin) of 0.2 and a leaf size of 10. +// (This means that nodes are split until they contain 10 or fewer points.) +// +// The std::move() means that `dataset` will be empty after this call, and no +// data will be copied during tree building. +// +// When C++20 is enabled, then the <> is not necessary and the following line +// will work: +// mlpack::SPTree tree(std::move(dataset), 0.2, 10); +mlpack::SPTree<> tree(std::move(dataset), 0.2, 10); + +// Print the bounding box of the root node. +std::cout << "Bounding box of root node:" << std::endl; +for (size_t i = 0; i < tree.Bound().Dim(); ++i) +{ + std::cout << " - Dimension " << i << ": [" << tree.Bound()[i].Lo() << ", " + << tree.Bound()[i].Hi() << "]." << std::endl; +} +std::cout << std::endl; + +// Print the number of descendant points of the root, and of each of its +// children. +std::cout << "Descendant points of root: " + << tree.NumDescendants() << "." << std::endl; +std::cout << "Descendant points of left child: " + << tree.Left()->NumDescendants() << "." << std::endl; +std::cout << "Descendant points of right child: " + << tree.Right()->NumDescendants() << "." << std::endl; +std::cout << std::endl; + +// Compute the center of the SPTree. +arma::vec center; +tree.Center(center); +std::cout << "Center of tree: " << center.t(); +``` + +--- + +Build two `SPTree`s on subsets of the corel dataset and compute minimum and +maximum distances between different nodes in the tree. + +```c++ +// See https://datasets.mlpack.org/corel-histogram.csv. +arma::mat dataset; +mlpack::data::Load("corel-histogram.csv", dataset, true); + +// Build trees on the first half and the second half of points. Use a tau +// (overlap) parameter of 0.3, which is tuned to this dataset, and a rho value +// of 0.6 to prevent the trees getting too deep. +mlpack::SPTree<> tree1(dataset.cols(0, dataset.n_cols / 2), 0.3, 20, 0.6); +mlpack::SPTree<> tree2(dataset.cols(dataset.n_cols / 2 + 1, dataset.n_cols - 1), + 0.3, 20, 0.6); + +// Compute the maximum distance between the trees. +std::cout << "Maximum distance between tree root nodes: " + << tree1.MaxDistance(tree2) << "." << std::endl; + +// Get the leftmost grandchild of the first tree's root---if it exists. +if (!tree1.IsLeaf() && !tree1.Child(0).IsLeaf()) +{ + mlpack::SPTree<>& node1 = tree1.Child(0).Child(0); + + // Get the rightmost grandchild of the second tree's root---if it exists. + if (!tree2.IsLeaf() && !tree2.Child(1).IsLeaf()) + { + mlpack::SPTree<>& node2 = tree2.Child(1).Child(1); + + // Print the minimum and maximum distance between the nodes. + mlpack::Range dists = node1.RangeDistance(node2); + std::cout << "Possible distances between two grandchild nodes: [" + << dists.Lo() << ", " << dists.Hi() << "]." << std::endl; + + // Print the minimum distance between the first node and the first + // descendant point of the second node. + const size_t descendantIndex = node2.Descendant(0); + const double descendantMinDist = + node1.MinDistance(node2.Dataset().col(descendantIndex)); + std::cout << "Minimum distance between grandchild node and descendant " + << "point: " << descendantMinDist << "." << std::endl; + + // Which child of node2 is closer to node1? + const size_t closerIndex = node2.GetNearestChild(node1); + if (closerIndex == 0) + std::cout << "The left child of node2 is closer to node1." << std::endl; + else if (closerIndex == 1) + std::cout << "The right child of node2 is closer to node1." << std::endl; + else // closerIndex == 2 in this case. + std::cout << "Both children of node2 are equally close to node1." + << std::endl; + + // And which child of node1 is further from node2? + const size_t furtherIndex = node1.GetFurthestChild(node2); + if (furtherIndex == 0) + std::cout << "The left child of node1 is further from node2." + << std::endl; + else if (furtherIndex == 1) + std::cout << "The right child of node1 is further from node2." + << std::endl; + else // furtherIndex == 2 in this case. + std::cout << "Both children of node1 are equally far from node2." + << std::endl; + } +} +``` + +--- + +Build an `SPTree` on 32-bit floating point data and save it to disk. + +```c++ +// See https://datasets.mlpack.org/corel-histogram.csv. +arma::fmat dataset; +mlpack::data::Load("corel-histogram.csv", dataset); + +// Build the SPTree using 32-bit floating point data as the matrix type. +// We will still use the default EmptyStatistic and EuclideanDistance +// parameters. +mlpack::SPTree tree(std::move(dataset), 0.1, 20, 0.95); + +// Save the tree to disk with the name 'tree'. +mlpack::data::Save("tree.bin", "tree", tree); + +std::cout << "Saved tree with " << tree.Dataset().n_cols << " points to " + << "'tree.bin'." << std::endl; +``` + +--- + +Load a 32-bit floating point `SPTree` from disk, then traverse it manually and +find the number of nodes whose children overlap. + +```c++ +// This assumes the tree has already been saved to 'tree.bin' (as in the example +// above). + +// This convenient typedef saves us a long type name! +using TreeType = mlpack::SPTree; + +TreeType tree; +mlpack::data::Load("tree.bin", "tree", tree); +std::cout << "Tree loaded with " << tree.NumDescendants() << " points." + << std::endl; + +// Recurse in a depth-first manner. Count both the total number of non-leaves, +// and the number of non-leaves that have overlapping children. +size_t overlapCount = 0; +size_t totalInternalNodeCount = 0; +std::stack stack; +stack.push(&tree); +while (!stack.empty()) +{ + TreeType* node = stack.top(); + stack.pop(); + + if (node->IsLeaf()) + continue; + + if (node->Overlap()) + ++overlapCount; + ++totalInternalNodeCount; + + stack.push(node->Left()); + stack.push(node->Right()); +} + +// Note that it would be possible to use TreeType::SingleTreeTraverser to +// perform the recursion above, but that is more well-suited for more complex +// tasks that require pruning and other non-trivial behavior; so using a simple +// stack is the better option here. + +// Print the results. +std::cout << overlapCount << " out of " << totalInternalNodeCount + << " internal nodes have overlapping children." << std::endl; +``` + +--- + +Use a defeatist traversal to find the approximate nearest neighbor of the third +and fourth points in the `corel-histogram` dataset. (Note: this can also be +done more easily with the `KNN` class! This example is a demonstration of how +to use the defeatist traverser.) + + + +For this example, we must first define a +[`RuleType` class](../../../developer/trees.md#rules). + +```c++ +// For simplicity, this only implements those methods required by single-tree +// traversals, and cannot be used with a dual-tree traversal. +// +// `.Reset()` must be called before any additional single-tree traversals after +// the first is run. +class SpillNearestNeighborRule +{ + public: + // Store the dataset internally. + SpillNearestNeighborRule(const arma::mat& dataset) : + dataset(dataset), + nearestNeighbor(size_t(-1)), + nearestDistance(DBL_MAX) { } + + // Compute the base case (point-to-point comparison). + double BaseCase(const size_t queryIndex, const size_t referenceIndex) + { + // Skip the base case if the points are the same. + if (queryIndex == referenceIndex) + return 0.0; + + const double dist = mlpack::EuclideanDistance::Evaluate( + dataset.col(queryIndex), dataset.col(referenceIndex)); + + if (dist < nearestDistance) + { + nearestNeighbor = referenceIndex; + nearestDistance = dist; + } + + return dist; + } + + // Score the given node in the tree; if it is sufficiently far away that it + // cannot contain a better nearest neighbor candidate, we can prune it. + template + double Score(const size_t queryIndex, const TreeType& referenceNode) const + { + const double minDist = referenceNode.MinDistance(dataset.col(queryIndex)); + if (minDist > nearestDistance) + return DBL_MAX; // Prune: this cannot contain a better candidate! + + return minDist; + } + + // Rescore the given node/point combination. Note that this will not be used + // by the defeatist traversal as it never backtracks, but we include it for + // completeness because the RuleType API requires it. + template + double Rescore(const size_t, const TreeType&, const double oldScore) const + { + if (oldScore > nearestDistance) + return DBL_MAX; // Prune: the node is too far away. + return oldScore; + } + + // This is required by defeatist traversals to select the best reference + // child to recurse into for overlapping nodes. + template + size_t GetBestChild(const size_t queryIndex, TreeType& referenceNode) + const + { + return referenceNode.GetNearestChild(dataset.col(queryIndex)); + } + + // We must perform at least two base cases in order to have a result. Note + // that this is two, and not one, because we skip base cases where the query + // and reference points are the same. That can only happen a maximum of once, + // so to ensure that we compare a query point to a different reference point + // at least once, we must return 2 here. + size_t MinimumBaseCases() const { return 2; } + + // Get the results (to be called after the traversal). + size_t NearestNeighbor() const { return nearestNeighbor; } + double NearestDistance() const { return nearestDistance; } + + // Reset the internal statistics for an additional traversal. + void Reset() + { + nearestNeighbor = size_t(-1); + nearestDistance = DBL_MAX; + } + + private: + const arma::mat& dataset; + + size_t nearestNeighbor; + double nearestDistance; +}; +``` + +```c++ +// See https://datasets.mlpack.org/corel-histogram.csv. +arma::mat dataset; +mlpack::data::Load("corel-histogram.csv", dataset, true); + +// Build two trees, one with a lot of overlap, and one with no overlap +// (e.g. tau = 0). +mlpack::SPTree<> tree1(dataset, 0.5, 10), tree2(dataset, 0.0, 10); + +// Construct the rule types, and then the traversals. +SpillNearestNeighborRule r1(dataset), r2(dataset); + +mlpack::SPTree<>::DefeatistSingleTreeTraverser + t1(r1), t2(r2); + +// Search for the approximate nearest neighbor of point 3 using both trees. +t1.Traverse(3, tree1); +t2.Traverse(3, tree2); + +std::cout << "Approximate nearest neighbor of point 3:" << std::endl; +std::cout << " - Spill tree with overlap 0.5 found: point " + << r1.NearestNeighbor() << ", distance " << r1.NearestDistance() + << "." << std::endl; + +std::cout << " - Spill tree with no overlap found: point " + << r2.NearestNeighbor() << ", distance " << r2.NearestDistance() + << "." << std::endl; + +// Now search for point 6. +r1.Reset(); +r2.Reset(); + +t1.Traverse(6, tree1); +t2.Traverse(6, tree2); + +std::cout << "Approximate nearest neighbor of point 6:" << std::endl; +std::cout << " - Spill tree with overlap 0.5 found: point " + << r1.NearestNeighbor() << ", distance " << r1.NearestDistance() + << "." << std::endl; + +std::cout << " - Spill tree with no overlap found: point " + << r2.NearestNeighbor() << ", distance " << r2.NearestDistance() + << "." << std::endl; +``` diff --git a/doc/user/core/trees/spill_tree.md b/doc/user/core/trees/spill_tree.md new file mode 100644 index 0000000000..571edc5b7d --- /dev/null +++ b/doc/user/core/trees/spill_tree.md @@ -0,0 +1,1086 @@ +# `SpillTree` + +The `SpillTree` class represents a generic multidimensional binary space +partitioning tree that allows overlapping volumes between nodes, also known as a +'hybrid spill tree'. It is heavily templatized to control splitting behavior +and other behaviors, and is the actual class underlying trees such as the +[`SPTree`](sp_tree.md). In general, the `SpillTree` class is not meant to be +used directly, and instead one of the handful of variants should be used +instead: + + * [`SPTree`](sp_tree.md) + * [`MeanSPTree`](mean_sp_tree.md) + * [`NonOrtSPTree`](non_ort_sp_tree.md) + * [`NonOrtMeanSPTree`](non_ort_mean_sp_tree.md) + +The `SpillTree` is similar to the [`BinarySpaceTree`](binary_space_tree.md), +except that the two children of a node are allowed to overlap, and thus a single +point can be contained in multiple branches of the tree. This can be useful to, +e.g., improve nearest neighbor performance when using [defeatist traversals +without backtracking](#tree-traversals). + +--- + +For users who want to use `SpillTree` directly or with custom behavior, +the full class is still detailed in the subsections below. `SpillTree` supports +the [TreeType API](../../../developer/trees.md#the-treetype-api) and can be used +with mlpack's tree-based algorithms, although using custom behavior may require +a template typedef. + + * [Template parameters](#template-parameters) + * [Constructors](#constructors) + * [Basic tree properties](#basic-tree-properties) + * [Bounding distances with the tree](#bounding-distances-with-the-tree) + * [`HyperplaneType`](#hyperplanetype) template parameter + * [`SplitType`](#splittype) template parameter + * [Tree traversals](#tree-traversals) + * [Example usage](#example-usage) + +## See also + + + + * [`SPTree`](sp_tree.md) + * [`MeanSPTree`](mean_sp_tree.md) + * [`NonOrtSPTree`](non_ort_sp_tree.md) + * [`NonOrtMeanSPTree`](non_ort_mean_sp_tree.md) + * [`BinarySpaceTree`](binary_space_tree.md) + * [An Investigation of Practical Approximate Nearest Neighbor Algorithms (pdf)](https://proceedings.neurips.cc/paper/2004/file/1102a326d5f7c9e04fc3c89d0ede88c9-Paper.pdf) + * [Tree-Independent Dual-Tree Algorithms (pdf)](https://www.ratml.org/pub/pdf/2013tree.pdf) + +## Template parameters + +The `SpillTree` class takes five template parameters. The first three of +these are required by the +[TreeType API](../../../developer/trees.md#template-parameters-required-by-the-treetype-policy) +(see also +[this more detailed section](../../../developer/trees.md#template-parameters)). The +full signature of the class is: + +``` +template class HyperplaneType, + template class SplitType> +class SpillTree; +``` + + * `DistanceType`: the [distance metric](../distances.md) to use for distance + computations. By default, this is + [`EuclideanDistance`](../distances.md#lmetric). + + * `StatisticType`: this holds auxiliary information in each tree node. By + default, [`EmptyStatistic`](binary_space_tree.md#emptystatistic) is used, + which holds no information. + - See the [`StatisticType`](binary_space_tree.md#statistictype) section in + the `BinarySpaceTree` documentation for more details. + + * `MatType`: the type of matrix used to represent points. Must be a type + matching the [Armadillo API](../../matrices.md). By default, `arma::mat` is + used, but other types such as `arma::fmat` or similar will work just fine. + + * `HyperplaneType`: the class defining the type of the hyperplane that will + split each node. By default, + [`AxisOrthogonalHyperplane`](#axisorthogonalhyperplane) is used. + - See the [`HyperplaneType`](#hyperplanetype) section for more details. + + * `SplitType`: the class defining how an individual `SpillTree` node + should be split. By default, [`MidpointSpaceSplit`](#midpointspacesplit) is + used. + - See the [`SplitType`](#splittype) section for more details. + +Note that the TreeType API requires trees to have only three template +parameters. In order to use a `SpillTree` with its five template parameters +with an mlpack algorithm that needs a TreeType, it is easiest to define a +template typedef: + +``` +template +using CustomTree = SpillTree +``` + +Here, `CustomHyperplaneType` and `CustomSplitType` are the desired hyperplane +type and split strategy. This is the way that all `SpillTree` variants (such as +[`SPTree`](sp_tree.md)) are defined. + +## Constructors + +`SpillTree`s are constructed by iteratively finding splitting hyperplanes, and +points within a margin of the hyperplane are assigned to *both* child nodes. +Unlike the constructors of +[`BinarySpaceTree`](binary_space_tree.md#constructors), the dataset is not +permuted during construction. + +--- + + * `node = SpillTree(data, tau=0.0, maxLeafSize=20, rho=0.7)` + - Construct a `SpillTree` on the given `data`, using the specified + hyperparameters to control tree construction behavior. + - Default template parameters are used, meaning that this tree will be a + [`SPTree`](sp_tree.md). + - By default, a reference to `data` is stored. If `data` goes out of scope + after tree construction, memory errors will occur! To avoid this, either + pass the dataset or a copy with `std::move()` (e.g. `std::move(data)`); + when doing this, `data` will be set to an empty matrix. + +--- + + * `node = SpillTree(data, tau=0.0, maxLeafSize=20, rho=0.7)` + - Construct a `SpillTree` on the given `data`, using custom template + parameters, and using the specified hyperparameters to control tree + construction behavior. + - By default, a reference to `data` is stored. If `data` goes out of scope + after tree construction, memory errors will occur! To avoid this, either + pass the dataset or a copy with `std::move()` (e.g. `std::move(data)`); + when doing this, `data` will be set to an empty matrix. + +--- + + * `node = SpillTree()` + - Construct an empty `SpillTree` with no children, no points, and + default template parameters. + +--- + +***Notes:*** + + - The name `node` is used here for `SpillTree` objects instead of `tree`, + because each `SpillTree` object is a single node in the tree. The + constructor returns the node that is the root of the tree. + + - Inserting individual points or removing individual points from a + `SpillTree` is not supported, because this generally results in a tree + with very suboptimal hyperplane splits. It is better to simply build a new + `SpillTree` on the modified dataset. For trees that support individual + insertion and deletions, see the [`RectangleTree`](rectangle_tree.md) class + and all its variants (e.g. [`RTree`](r_tree.md), + [`RStarTree`](r_star_tree.md), etc.). + + - See also the + [developer documentation on tree constructors](../../../developer/trees.md#constructors-and-destructors). + +--- + +### Constructor parameters: + +| **name** | **type** | **description** | **default** | +|----------|----------|-----------------|-------------| +| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. | _(N/A)_ | +| `tau` | `double` | Width of spill margin: points within `tau` of the splitting hyperplane of a node will be contained in both left and right children. | `0.0` | +| `maxLeafSize` | `size_t` | Maximum number of points to store in each leaf. | `20` | +| `rho` | `double` | Balance threshold. When splitting, if either overlapping node would contain a fraction of more than `rho` of the points, a non-overlapping split is performed. Must be in the range `[0.0, 1.0)`. | `0.7` | + +***Caveats***: + + * `tau` must be manually tuned for the properties of each dataset; the default, + `0.0`, will never allow overlap between nodes (and thus the created tree will + essentially be a non-overlapping [`BinarySpaceTree`](binary_space_tree.md)). + + * If `tau` is set too large, nodes will overlap too much and search quality + will be degraded. + + * `rho` implicitly controls the depth of the tree by forcing very overlapping + children to be non-overlapping. As `rho` gets closer to `1`, more overlap is + allowed, which in turn makes the tree deeper. If `rho` is set to `0.5` or + less, then all splits will be non-overlapping (and the tree will essentially + be a [`BinarySpaceTree`](binary_space_tree.md)). + +## Basic tree properties + +Once a `SpillTree` object is constructed, various properties of the tree +can be accessed or inspected. Many of these functions are required by the +[TreeType API](../../../developer/trees.md#the-treetype-api). + +### Navigating the tree + + * `node.NumChildren()` returns the number of children in `node`. This is + either `2` if `node` has children, or `0` if `node` is a leaf. + + * `node.IsLeaf()` returns a `bool` indicating whether or not `node` is a leaf. + + * `node.Child(i)` returns a `SpillTree&` that is the `i`th child. + - `i` must be `0` or `1`. + - This function should only be called if `node.NumChildren()` is not `0` + (e.g. if `node` is not a leaf). Note that this returns a valid + `SpillTree&` that can itself be used just like the root node of the + tree! + - `node.Left()` and `node.Right()` are convenience functions specific to + `SpillTree` that will return `SpillTree*` (pointers) to the left and right + children, respectively, or `NULL` if `node` has no children. + + * `node.Parent()` will return a `SpillTree*` that points to the parent of + `node`, or `NULL` if `node` is the root of the `SpillTree`. + +--- + +### Accessing members of a tree + + * `node.Overlap()` will return a `bool` that is `true` if `node`'s children are + overlapping, and `false` otherwise. + + * `node.Hyperplane()` will return a `HyperplaneType&` object that represents + the splitting hyperplane of `node`. + - All points in `node.Left()` are to the left of `node.Hyperplane()` if + `node.Overlap()` is `false`; otherwise, all points in `node.Left()` are to + the left of `node.Hyperplane() + tau`. + - All points in `node.Right()` are to the right of `node.Hyperplane()` if + `node.Overlap()` is `false`; otherwise, all points in `node.Right()` are to + the right of `node.Hyperplane() - tau`. + + * `node.Bound()` will return a + [`const HRectBound&`](binary_space_tree.md#hrectbound) representing the + bounding box associated with `node`. + - If a [custom `HyperplaneType`](#hyperplanetype) is specified, then the + `BoundType` associated with that hyperplane type is returned instead. + - If a [custom `DistanceType` and/or `MatType`](#template-parameters) are + specified, then a `const HRectBound&` is returned + (or a `BoundType` with that `DistanceType`, if a custom `HyperplaneType` + was also specified). + * `ElemType` is the element type of the specified `MatType` (e.g. `double` + for `arma::mat`, `float` for `arma::fmat`, etc.). + + * `node.Stat()` will return a `StatisticType&` holding the statistics of the + node that were computed during tree construction. + + * `node.Distance()` will return a `DistanceType&`. + +See also the +[developer documentation](../../../developer/trees.md#basic-tree-functionality) +for basic tree functionality in mlpack. + +--- + +### Accessing data held in a tree + + * `node.Dataset()` will return a `const MatType&` that is the dataset the + tree was built on. + + * `node.NumPoints()` returns a `size_t` indicating the number of points held + directly in `node`. + - If `node` is not a leaf, this will return `0`, as `SpillTree` only holds + points directly in its leaves. + - If `node` is a leaf, then the number of points will be less than or equal + to the `maxLeafSize` that was specified when the tree was constructed. + + * `node.Point(i)` returns a `size_t` indicating the index of the `i`'th point + in `node.Dataset()`. + - `i` must be in the range `[0, node.NumPoints() - 1]` (inclusive). + - `node` must be a leaf (as non-leaves do not hold any points). + - The `i`'th point in `node` can then be accessed as + `node.Dataset().col(node.Point(i))`. + - Accessing the actual `i`'th point itself can be done with, e.g., + `node.Dataset().col(node.Point(i))`. + + * `node.NumDescendants()` returns a `size_t` indicating the number of points + held in all descendant leaves of `node`. + - If `node` is the root of the tree, then `node.NumDescendants()` will be + equal to `node.Dataset().n_cols`. + + * `node.Descendant(i)` returns a `size_t` indicating the index of the `i`'th + descendant point in `node.Dataset()`. + - `i` must be in the range `[0, node.NumDescendants() - 1]` (inclusive). + - `node` does not need to be a leaf. + - The `i`'th descendant point in `node` can then be accessed as + `node.Dataset().col(node.Descendant(i))`. + - Accessing the actual `i`'th descendant itself can be done with, e.g., + `node.Dataset().col(node.Descendant(i))`. + +--- + +### Accessing computed bound quantities of a tree + +The following quantities are cached for each node in a `SpillTree`, and so +accessing them does not require any computation. In the documentation below, +`ElemType` is the element type of the given `MatType`; e.g., if `MatType` is +`arma::mat`, then `ElemType` is `double`. + + * `node.FurthestPointDistance()` returns an `ElemType` representing the + distance between the center of the bound of `node` and the furthest point + held by `node`. + - If `node` is not a leaf, this returns 0 (because `node` does not hold any + points). + + * `node.FurthestDescendantDistance()` returns an `ElemType` representing the + distance between the center of the bound of `node` and the furthest + descendant point held by `node`. + + * `node.MinimumBoundDistance()` returns an `ElemType` representing the minimum + possible distance from the center of the node to any edge of its bound. + + * `node.ParentDistance()` returns an `ElemType` representing the distance + between the center of the bound of `node` and the center of the bound of its + parent. + - If `node` is the root of the tree, `0` is returned. + +***Note:*** for more details on each bound quantity, see the [developer +documentation](../../../developer/trees.md#complex-tree-functionality-and-bounds) +on bound quantities for trees. + +--- + +### Other functionality + + * `node.Center(center)` computes the center of the bound of `node` and stores + it in `center`. + - `center` should be of type `arma::Col&`, where `ElemType` is the + element type of the specified `MatType`. + - `center` will be set to have size equivalent to the dimensionality of the + dataset held by `node`. + - This is equivalent to calling `node.Bound().Center(center)`. + + * A `SpillTree` can be serialized with + [`data::Save()` and `data::Load()`](../../load_save.md#mlpack-objects). + +## Bounding distances with the tree + +The primary use of trees in mlpack is bounding distances to points or other tree +nodes. The following functions can be used for these tasks. + + * `node.GetNearestChild(point)` + * `node.GetFurthestChild(point)` + - Return a `size_t` indicating the index of the child (`0` for left, `1` for + right) that is closest to (or furthest from) `point`, with respect + to the `MinDistance()` (or `MaxDistance()`) function. + - If there is a tie, `0` (the left child) is returned. + - If `node` is a leaf, `0` is returned. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + + * `node.GetNearestChild(other)` + * `node.GetFurthestChild(other)` + - Return a `size_t` indicating the index of the child (`0` for left, `1` for + right) that is closest to (or furthest from) the `SpillTree` node + `other`, with respect to the `MinDistance()` (or `MaxDistance()`) function. + - If there is a tie, `0` (the left child) is returned. + - If `node` is a leaf, `0` is returned. + +--- + + * `node.MinDistance(point)` + * `node.MinDistance(other)` + - Return a `double` indicating the minimum possible distance between `node` + and `point`, or the `SpillTree` node `other`. + - This is equivalent to the minimum possible distance between any point + contained in the bounding hyperrectangle of `node` and `point`, or between + any point contained in the bounding hyperrectangle of `node` and any point + contained in the bounding hyperrectangle of `other`. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + + * `node.MaxDistance(point)` + * `node.MaxDistance(other)` + - Return a `double` indicating the maximum possible distance between `node` + and `point`, or the `SpillTree` node `other`. + - This is equivalent to the maximum possible distance between any point + contained in the bounding hyperrectangle of `node` and `point`, or between + any point contained in the bounding hyperrectangle of `node` and any point + contained in the bounding hyperrectangle of `other`. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + + * `node.RangeDistance(point)` + * `node.RangeDistance(other)` + - Return a [`RangeType`](../math.md#range) whose lower bound is + `node.MinDistance(point)` or `node.MinDistance(other)`, and whose upper + bound is `node.MaxDistance(point)` or `node.MaxDistance(other)`. + - `ElemType` is the element type of `MatType`. + - `point` should be a column vector type of the same type as `MatType`. + (e.g., if `MatType` is `arma::mat`, then `point` should be an `arma::vec`.) + +## Tree traversals + +Like every mlpack tree, the `SpillTree` class provides a [single-tree and +dual-tree traversal](../../../developer/trees.md#traversals) that can be paired +with a [`RuleType` class](../../../developer/trees.md#rules) to implement a +single-tree or dual-tree algorithm. + + * `SpillTree::SingleTreeTraverser` + - Implements a depth-first single-tree traverser. + + * `SpillTree::DualTreeTraverser` + - Implements a dual-depth-first dual-tree traverser. + +However, the spill tree is primarily useful because the overlapping nodes allow +*defeatist* search to be effective. Defeatist search is non-backtracking: the +tree is traversed to one leaf only. For example, finding the approximate +nearest neighbor of a point `p` with defeatist search is done by recursing in +the tree, choosing the child with smallest minimum distance to `p`, and when a +leaf is encountered, choosing the closest point in the leaf to `p` as the +nearest neighbor. This is the strategy used in the +[original spill tree paper (pdf)](https://proceedings.neurips.cc/paper/2004/file/1102a326d5f7c9e04fc3c89d0ede88c9-Paper.pdf). + +Defeatist traversers, matching the API for a regular +[traversal](../../../developer/trees.md#traversals) are made available as the +following two classes: + + * `SpillTree::DefeatistSingleTreeTraverser` + - Implements a depth-first single-tree defeatist traverser with no + backtracking. Traversal will terminate after the first leaf is visited. + + * `SpillTree::DefeatistDualTreeTraverser` + - Implements a dual-depth-first dual-tree defeatist traversal with no + backtracking. For each query leaf node, traversal will terminate after the + first reference leaf node is visited. + +Any [`RuleType`](../../../developer/trees.md#rules) that is being used with a +defeatist traversal, in addition to the functions required by the `RuleType` +API, must implement the following functions: + +``` +// This is only required for single-tree defeatist traversals. +// It should return the index of the branch that should be chosen for the given +// query point and reference node. +template +size_t GetBestChild(const VecType& queryPoint, TreeType& referenceNode); + +// This is only required for dual-tree defeatist traversals. +// It should return the index of the best child of the reference node that +// should be chosen for the given query node. +template +size_t GetBestChild(TreeType& queryNode, TreeType& referenceNode); + +// Return the minimum number of base cases (point-to-point computations) that +// are required during the traversal. +size_t MinimumBaseCases(); +``` + +## `HyperplaneType` + +Each node in a `SpillTree` corresponds to some region in space that contains all +of the descendant points in the node. Similar to the [`KDTree`](kdtree.md), this +region is a hyperrectangle; however, instead of representing that hyperrectangle +explicitly like the `KDTree` with the +[`HRectBound`](binary_space_tree.md#hrectbound) class, the `SpillTree` +represents the region *implicitly*, with each node storing only the hyperplane +and margin required to determine whether a point belongs to the left node, the +right node, or both. + +The type of hyperplane (e.g. axis-aligned or arbitrary) can be controlled by the +`HyperplaneType` template parameter. mlpack supplies two drop-in classes that +can be used for `HyperplaneType`, and it is also possible to write a custom +`HyperplaneType`: + + * [`AxisOrthogonalHyperplane`](#axisorthogonalhyperplane): uses hyperplanes + that are axis-orthogonal (or axis-aligned). + * [`Hyperplane`](#hyperplane): uses arbitrary hyperplanes specified by any + vector. + * [Custom `HyperplaneType`s](#custom-hyperplanetypes): implement a fully custom + `HyperplaneType` class + +### `AxisOrthogonalHyperplane` + +The `AxisOrthogonalHyperplane` class is used to provide an axis-orthogonal split +for a `SpillTree`. That is, whether or not a point is on the left or right side +of the split is a very efficient computation using only a single dimension of +the data. + + * The `AxisOrthogonalHyperplane` class defines the following two typedefs: + - `AxisOrthogonalHyperplane::BoundType`, which is the type of the bound used + by the spill tree with this hyperplane, is + [`HRectBound`](binary_space_tree.md#hrectbound), or + `HRectBound` if custom + [`DistanceType` and/or `MatType`](#template-parameters) are specified. + + - `AxisOrthogonalHyperplane::ProjVectorType` is `AxisParallelProjVector`, a + class that simply holds the index of the dimension of the projection + vector. + * For more details, see + [the source code](/src/mlpack/core/tree/space_split/projection_vector.hpp). + + * An `AxisOrthogonalHyperplane` object `h` (e.g. returned with + `node.Hyperplane()`) has the following members: + + - `h.Project(point)` returns a `double` that is the orthogonal projection of + `point` onto the tangent vector of the hyperplane `h`. + + - `h.Left(point)` returns `true` if `point` is to the left of `h`. + + - `h.Right(point)` returns `true` if `point` is to the right of `h`. + + - `h.Left(bound)` returns `true` if `bound` (an + `AxisOrthogonalHyperplane::BoundType`; see the bullet point above) is to + the left of `h`. + + - `h.Right(bound)` returns `true` if `bound` (an + `AxisOrthogonalHyperplane::BoundType`; see the bullet point above) is to + the right of `h`. + + * An `AxisOrthogonalHyperplane` object can be serialized with + [`data::Save()` and `data::Load()`](../../load_save.md#mlpack-objects). + +For more details, see the +[the source code](/src/mlpack/core/tree/space_split/hyperplane.hpp). + +### `Hyperplane` + +The `Hyperplane` class is used to provide an arbitrary hyperplane split for a +`SpillTree`. The computation of whether or not a point is on the left or right +side of the split is less efficient than +[`AxisOrthogonalHyperplane`](#axisorthogonalhyperplane), but `Hyperplane` is +able to represent any possible hyperplane. + + * The `Hyperplane` class defines the two following typedefs: + - `Hyperplane::BoundType`, which is the type of the bound used by the spill + tree with this hyperplane, is + [`BallBound`](binary_space_tree.md#ballbound), or `BallBound` + if a custom [`DistanceType`](#template-parameters) is specified. + + - `Hyperplane::ProjVectorType` is `ProjVector<>`, an arbitrary projection + vector class that wraps an `arma::vec`. + * If a custom `MatType` is specified, then `Hyperplane::ProjVectorType` is + `ProjVector`, which wraps a vector of the same type as + `MatType`. + * For more details, see + [the source code](/src/mlpack/core/tree/space_split/projection_vector.hpp). + + * A `Hyperplane` object `h` (e.g. returned with `node.Hyperplane()`) has the + following members: + + - `h.Project(point)` returns a `double` that is the orthogonal projection of + `point` onto the tangent vector of the hyperplane `h`. + + - `h.Left(point)` returns `true` if `point` is to the left of `h`. + + - `h.Right(point)` returns `true` if `point` is to the right of `h`. + + - `h.Left(bound)` returns `true` if `bound` (an + `AxisOrthogonalHyperplane::BoundType`; see the bullet point above) is to + the left of `h`. + + - `h.Right(bound)` returns `true` if `bound` (a `Hyperplane::BoundType`; see + the bullet point above) is to the right of `h`. + + * A `Hyperplane` object can be serialized with + [`data::Save()` and `data::Load()`](../../load_save.md#mlpack-objects). + +For more details, see the +[the source code](/src/mlpack/core/tree/space_split/hyperplane.hpp). + +### Custom `HyperplaneType`s + +Custom hyperplane types for a spill tree can be implemented via the +`HyperplaneType` template parameter. By default, the +[`AxisOrthogonalHyperplane`](#axisorthogonalhyperplane) hyperplane type is used, +but it is also possible to implement and use a custom `HyperplaneType`. Any +custom `HyperplaneType` class must implement the following signature: + +```c++ +// NOTE: the custom HyperplaneType class must take two template parameters. +template +class HyperplaneType +{ + public: + // The hyperplane type must specify these two public typedefs, which are used + // by the spill tree and the splitting strategy. + // + // Substitute HRectBound and ProjVector with your choices. + using BoundType = mlpack::HRectBound; + using ProjVectorType = mlpack::ProjVector; + + // Empty constructor, which will construct an empty or default hyperplane. + HyperplaneType(); + + // Construct the HyperplaneType with the given projection vector and split + // value along that projection. + HyperplaneType(const ProjVectorType& projVector, double splitVal); + + // Compute the projection of the given point (an `arma::vec` or similar type + // matching the Armadillo API and element type of `MatType`) onto the vector + // tangent to the hyperplane. + template + double Project(const VecType& point) const; + + // Return true if the point (an `arma::vec` or similar type matching the + // Armadillo API and element type of `MatType`) falls to the left of the + // hyperplane. + template + double Left(const VecType& point) const; + + // Return true if the point (an `arma::vec` or similar type matching the + // Armadillo API and element type of `MatType`) falls to the right of the + // hyperplane. + template + double Right(const VecType& point) const; + + // Return true if the given bound is fully to the left of the hyperplane. + bool Left(const BoundType& bound) const; + + // Return true if the given bound is fully to the right of the hyperplane. + bool Right(const BoundType& bound) const; + + // Serialize the hyperplane using cereal. + template + void serialize(Archive& ar, const uint32_t version); +}; +``` + +## `SplitType` + +The `SplitType` template parameter controls the algorithm used to split each +node of a `SpillTree` while building. The splitting strategy used can be +entirely arbitrary---the `SplitType` only needs to compute a +[`HyperplaneType`](#hyperplanetype) to split a set of points. + +mlpack provides two drop-in choices for `SplitType`, and it is also possible +to write a fully custom split: + + * [`MidpointSpaceSplit`](#midpointspacesplit): split a set of points using a + hyperplane built on the midpoint (median) of points in a dataset. + * [`MeanSpaceSplit`](#meanspacesplit): split a set of points using a hyperplane + built on the mean (average) of points in a dataset. + * [Custom `SplitType`s](#custom-splittypes): implement a fully custom + `SplitType` class + +### `MidpointSpaceSplit` + +The `MidpointSpaceSplit` class is a splitting strategy that can be used by +`SpillTree`. It is the default strategy for splitting [`SPTree`s](sp_tree.md) +and [`NonOrtSPTree`s](non_ort_sp_tree.md). + +The splitting strategy for the `MidpointSpaceSplit` class is, given a set of +points: + + * If [`AxisOrthogonalHyperplane`](#axisorthogonalhyperplane) is being used, + then select the dimension with the maximum width, and use the midpoint of the + points' values in that dimension. + + * If [`Hyperplane`](#hyperplane) is being used, then estimate the furthest two + points in the dataset by random sampling, and use the vector connecting those + points as the tangent vector to the hyperplane. The midpoint of the points + projected onto this hyperplane is used as the split value. + +Note that `MidpointSpaceSplit` can only be used with a `HyperplaneType` with +`HyperplaneType::ProjVectorType` as either `AxisAlignedProjVector` or +`ProjVector`. + +For implementation details, see +[the source code](/src/mlpack/core/tree/space_split/midpoint_space_split_impl.hpp). + +### `MeanSpaceSplit` + +The `MeanSpaceSplit` class is a splitting strategy that can be used by +`SpillTree`. It is the splitting strategy used by the +[`MeanSPTree`](mean_sp_tree.md) and the +[`NonOrtMeanSPTree`](non_ort_mean_sp_tree.md) classes. + +The splitting strategy for the `MeanSpaceSplit` class is, given a set of +points: + + * If [`AxisOrthogonalHyperplane`](#axisorthogonalhyperplane) is being used, + then select the dimension with the maximum width, and use the mean of the + points' values in that dimension. + + * If [`Hyperplane`](#hyperplane) is being used, then estimate the furthest two + points in the dataset by random sampling, and use the vector connecting those + points as the tangent vector to the hyperplane. The mean of the points + projected onto this hyperplane is used as the split value. + +Note that `MeanSpaceSplit` can only be used with a `HyperplaneType` with +`HyperplaneType::ProjVectorType` as either `AxisAlignedProjVector` or +`ProjVector`. + +For implementation details, see +[the source code](/src/mlpack/core/tree/space_split/mean_space_split_impl.hpp). + +### Custom `SplitType`s + +Custom split strategies for a spill tree can be implemented via the +`SplitType` template parameter. By default, the +[`MidpointSpaceSplit`](#midpointspacesplit) splitting strategy is used, but it +is also possible to implement and use a custom `SplitType`. Any custom +`SplitType` class must implement the following signature: + +```c++ +// NOTE: the custom SplitType class must take two template parameters. +template +class SplitType +{ + public: + // The SplitType class is only required to provide one static function. + + // Create a splitting hyperplane and store it in the given `HyperplaneType`, + // using the given data and bounding box `bound`. `data` will be an Armadillo + // matrix that is the entire dataset, and `points` are the indices of points + // in `data` that should be split. + template + static bool SplitSpace( + const typename HyperplaneType::BoundType& bound, + const MatType& data, + const arma::Col& points, + HyperplaneType& hyp); +}; +``` + +## Example usage + +The `SpillTree` class is only really necessary when a custom hyperplane type or +custom splitting strategy is intended to be used. For simpler use cases, one of +the typedefs of `SpillTree` (such as [`SPTree`](sp_tree.md)) will suffice. + +For this reason, all of the examples below explicitly specify all five template +parameters of `SPTree`. +[Writing a custom hyperplane type](#custom-hyperplanetypes) and +[writing a custom splitting strategy](#custom-splittypes) are discussed +in the previous sections. Each of the parameters in the examples below can be +trivially changed for different behavior. + +--- + +Build a `SpillTree` on the `cloud` dataset and print basic statistics about the +tree. + +```c++ +// See https://datasets.mlpack.org/cloud.csv. +arma::mat dataset; +mlpack::data::Load("cloud.csv", dataset, true); + +// Build the spill tree with a tau (margin) of 0.2 and a leaf size of 10. +// (This means that nodes are split until they contain 10 or fewer points.) +// +// The std::move() means that `dataset` will be empty after this call, and no +// data will be copied during tree building. +mlpack::SpillTree tree(std::move(dataset), 0.2, 10); + +// Print the bounding box of the root node. +std::cout << "Bounding box of root node:" << std::endl; +for (size_t i = 0; i < tree.Bound().Dim(); ++i) +{ + std::cout << " - Dimension " << i << ": [" << tree.Bound()[i].Lo() << ", " + << tree.Bound()[i].Hi() << "]." << std::endl; +} +std::cout << std::endl; + +// Print the number of descendant points of the root, and of each of its +// children. +std::cout << "Descendant points of root: " + << tree.NumDescendants() << "." << std::endl; +std::cout << "Descendant points of left child: " + << tree.Left()->NumDescendants() << "." << std::endl; +std::cout << "Descendant points of right child: " + << tree.Right()->NumDescendants() << "." << std::endl; +std::cout << std::endl; + +// Compute the center of the SpillTree. +arma::vec center; +tree.Center(center); +std::cout << "Center of tree: " << center.t(); +``` + +--- + +Build two `SpillTree`s on subsets of the corel dataset and compute minimum +and maximum distances between different nodes in the tree. + +```c++ +// See https://datasets.mlpack.org/corel-histogram.csv. +arma::mat dataset; +mlpack::data::Load("corel-histogram.csv", dataset, true); + +// Convenience typedef for the tree type. +using TreeType = mlpack::SpillTree; + +// Build trees on the first half and the second half of points. Use a tau +// (overlap) parameter of 0.3, which is tuned to this dataset, and a rho value +// of 0.6 to prevent the trees getting too deep. +TreeType tree1(dataset.cols(0, dataset.n_cols / 2), 0.3, 20, 0.6); +TreeType tree2(dataset.cols(dataset.n_cols / 2 + 1, dataset.n_cols - 1), + 0.3, 20, 0.6); + +// Compute the maximum distance between the trees. +std::cout << "Maximum distance between tree root nodes: " + << tree1.MaxDistance(tree2) << "." << std::endl; + +// Get the leftmost grandchild of the first tree's root---if it exists. +if (!tree1.IsLeaf() && !tree1.Child(0).IsLeaf()) +{ + TreeType& node1 = tree1.Child(0).Child(0); + + // Get the rightmost grandchild of the second tree's root---if it exists. + if (!tree2.IsLeaf() && !tree2.Child(1).IsLeaf()) + { + TreeType& node2 = tree2.Child(1).Child(1); + + // Print the minimum and maximum distance between the nodes. + mlpack::Range dists = node1.RangeDistance(node2); + std::cout << "Possible distances between two grandchild nodes: [" + << dists.Lo() << ", " << dists.Hi() << "]." << std::endl; + + // Print the minimum distance between the first node and the first + // descendant point of the second node. + const size_t descendantIndex = node2.Descendant(0); + const double descendantMinDist = + node1.MinDistance(node2.Dataset().col(descendantIndex)); + std::cout << "Minimum distance between grandchild node and descendant " + << "point: " << descendantMinDist << "." << std::endl; + + // Which child of node2 is closer to node1? + const size_t closerIndex = node2.GetNearestChild(node1); + if (closerIndex == 0) + std::cout << "The left child of node2 is closer to node1." << std::endl; + else if (closerIndex == 1) + std::cout << "The right child of node2 is closer to node1." << std::endl; + else // closerIndex == 2 in this case. + std::cout << "Both children of node2 are equally close to node1." + << std::endl; + + // And which child of node1 is further from node2? + const size_t furtherIndex = node1.GetFurthestChild(node2); + if (furtherIndex == 0) + std::cout << "The left child of node1 is further from node2." + << std::endl; + else if (furtherIndex == 1) + std::cout << "The right child of node1 is further from node2." + << std::endl; + else // furtherIndex == 2 in this case. + std::cout << "Both children of node1 are equally far from node2." + << std::endl; + } +} +``` + +--- + +Build a `SpillTree` on 32-bit floating point data and save it to disk. + +```c++ +// See https://datasets.mlpack.org/corel-histogram.csv. +arma::fmat dataset; +mlpack::data::Load("corel-histogram.csv", dataset); + +// Build the SpillTree using 32-bit floating point data as the matrix type. +// We will still use the default EmptyStatistic and EuclideanDistance +// parameters. +mlpack::SpillTree tree( + std::move(dataset), 0.1, 20, 0.95); + +// Save the tree to disk with the name 'tree'. +mlpack::data::Save("tree.bin", "tree", tree); + +std::cout << "Saved tree with " << tree.Dataset().n_cols << " points to " + << "'tree.bin'." << std::endl; +``` + +--- + +Load a 32-bit floating point `SpillTree` from disk, then traverse it +manually and find the number of nodes whose children overlap. + +```c++ +// This assumes the tree has already been saved to 'tree.bin' (as in the example +// above). + +// This convenient typedef saves us a long type name! +using TreeType = mlpack::SpillTree; + +TreeType tree; +mlpack::data::Load("tree.bin", "tree", tree); +std::cout << "Tree loaded with " << tree.NumDescendants() << " points." + << std::endl; + +// Recurse in a depth-first manner. Count both the total number of non-leaves, +// and the number of non-leaves that have overlapping children. +size_t overlapCount = 0; +size_t totalInternalNodeCount = 0; +std::stack stack; +stack.push(&tree); +while (!stack.empty()) +{ + TreeType* node = stack.top(); + stack.pop(); + + if (node->IsLeaf()) + continue; + + if (node->Overlap()) + ++overlapCount; + ++totalInternalNodeCount; + + stack.push(node->Left()); + stack.push(node->Right()); +} + +// Note that it would be possible to use TreeType::SingleTreeTraverser to +// perform the recursion above, but that is more well-suited for more complex +// tasks that require pruning and other non-trivial behavior; so using a simple +// stack is the better option here. + +// Print the results. +std::cout << overlapCount << " out of " << totalInternalNodeCount + << " internal nodes have overlapping children." << std::endl; +``` + +--- + +Use a defeatist traversal to find the approximate nearest neighbor of the third +and fourth points in the `corel-histogram` dataset. (Note: this can also be +done more easily with the `KNN` class! This example is a demonstration of how +to use the defeatist traverser.) + + + +For this example, we must first define a +[`RuleType` class](../../../developer/trees.md#rules). + +```c++ +// For simplicity, this only implements those methods required by single-tree +// traversals, and cannot be used with a dual-tree traversal. +// +// `.Reset()` must be called before any additional single-tree traversals after +// the first is run. +class SpillNearestNeighborRule +{ + public: + // Store the dataset internally. + SpillNearestNeighborRule(const arma::mat& dataset) : + dataset(dataset), + nearestNeighbor(size_t(-1)), + nearestDistance(DBL_MAX) { } + + // Compute the base case (point-to-point comparison). + double BaseCase(const size_t queryIndex, const size_t referenceIndex) + { + // Skip the base case if the points are the same. + if (queryIndex == referenceIndex) + return 0.0; + + const double dist = mlpack::EuclideanDistance::Evaluate( + dataset.col(queryIndex), dataset.col(referenceIndex)); + + if (dist < nearestDistance) + { + nearestNeighbor = referenceIndex; + nearestDistance = dist; + } + + return dist; + } + + // Score the given node in the tree; if it is sufficiently far away that it + // cannot contain a better nearest neighbor candidate, we can prune it. + template + double Score(const size_t queryIndex, const TreeType& referenceNode) const + { + const double minDist = referenceNode.MinDistance(dataset.col(queryIndex)); + if (minDist > nearestDistance) + return DBL_MAX; // Prune: this cannot contain a better candidate! + + return minDist; + } + + // Rescore the given node/point combination. Note that this will not be used + // by the defeatist traversal as it never backtracks, but we include it for + // completeness because the RuleType API requires it. + template + double Rescore(const size_t, const TreeType&, const double oldScore) const + { + if (oldScore > nearestDistance) + return DBL_MAX; // Prune: the node is too far away. + return oldScore; + } + + // This is required by defeatist traversals to select the best reference + // child to recurse into for overlapping nodes. + template + size_t GetBestChild(const size_t queryIndex, TreeType& referenceNode) + const + { + return referenceNode.GetNearestChild(dataset.col(queryIndex)); + } + + // We must perform at least two base cases in order to have a result. Note + // that this is two, and not one, because we skip base cases where the query + // and reference points are the same. That can only happen a maximum of once, + // so to ensure that we compare a query point to a different reference point + // at least once, we must return 2 here. + size_t MinimumBaseCases() const { return 2; } + + // Get the results (to be called after the traversal). + size_t NearestNeighbor() const { return nearestNeighbor; } + double NearestDistance() const { return nearestDistance; } + + // Reset the internal statistics for an additional traversal. + void Reset() + { + nearestNeighbor = size_t(-1); + nearestDistance = DBL_MAX; + } + + private: + const arma::mat& dataset; + + size_t nearestNeighbor; + double nearestDistance; +}; +``` + +```c++ +// See https://datasets.mlpack.org/corel-histogram.csv. +arma::mat dataset; +mlpack::data::Load("corel-histogram.csv", dataset, true); + +typedef mlpack::SpillTree TreeType; + +// Build two trees, one with a lot of overlap, and one with no overlap +// (e.g. tau = 0). +TreeType tree1(dataset, 0.5, 10), tree2(dataset, 0.0, 10); + +// Construct the rule types, and then the traversals. +SpillNearestNeighborRule r1(dataset), r2(dataset); + +TreeType::DefeatistSingleTreeTraverser t1(r1); +TreeType::DefeatistSingleTreeTraverser t2(r2); + +// Search for the approximate nearest neighbor of point 3 using both trees. +t1.Traverse(3, tree1); +t2.Traverse(3, tree2); + +std::cout << "Approximate nearest neighbor of point 3:" << std::endl; +std::cout << " - Spill tree with overlap 0.5 found: point " + << r1.NearestNeighbor() << ", distance " << r1.NearestDistance() + << "." << std::endl; + +std::cout << " - Spill tree with no overlap found: point " + << r2.NearestNeighbor() << ", distance " << r2.NearestDistance() + << "." << std::endl; + +// Now search for point 6. +r1.Reset(); +r2.Reset(); + +t1.Traverse(6, tree1); +t2.Traverse(6, tree2); + +std::cout << "Approximate nearest neighbor of point 6:" << std::endl; +std::cout << " - Spill tree with overlap 0.5 found: point " + << r1.NearestNeighbor() << ", distance " << r1.NearestDistance() + << "." << std::endl; + +std::cout << " - Spill tree with no overlap found: point " + << r2.NearestNeighbor() << ", distance " << r2.NearestDistance() + << "." << std::endl; +``` diff --git a/doc/user/core/trees/ub_tree.md b/doc/user/core/trees/ub_tree.md index 025fa5451a..42b2c021cf 100644 --- a/doc/user/core/trees/ub_tree.md +++ b/doc/user/core/trees/ub_tree.md @@ -133,13 +133,11 @@ different. bounding boxes. It is better to simply build a new `UBTree` on the modified dataset. For trees that support individual insertion and deletions, see the [`RectangleTree`](rectangle_tree.md) class and all its variants (e.g. - [`RTree`](r_tree.md), `RStarTree`, etc.). + [`RTree`](r_tree.md), [`RStarTree`](r_star_tree.md), etc.). - See also the [developer documentation on tree constructors](../../../developer/trees.md#constructors-and-destructors). - - --- ### Constructor parameters: @@ -427,7 +425,7 @@ mlpack::data::Load("cloud.csv", dataset, true); // // Note that the '<>' isn't necessary if C++20 is being used (e.g. // `mlpack::UBTree tree(...)` will work fine in C++20 or newer). -mlpack::UBTree<> tree(std::move(dataset)); +mlpack::UBTree<> tree(std::move(dataset), 10); // Print the bounding box of the root node. std::cout << "Outer bounding box of root node:" << std::endl; diff --git a/doc/user/core/trees/vptree.md b/doc/user/core/trees/vptree.md index 04b4675170..9a5c7361ab 100644 --- a/doc/user/core/trees/vptree.md +++ b/doc/user/core/trees/vptree.md @@ -128,13 +128,11 @@ different. very loose bounding balls. It is better to simply build a new `VPTree` on the modified dataset. For trees that support individual insertion and deletions, see the [`RectangleTree`](rectangle_tree.md) class and all its - variants (e.g. [`RTree`](r_tree.md), `RStarTree`, etc.). + variants (e.g. [`RTree`](r_tree.md), [`RStarTree`](r_star_tree.md), etc.). - See also the [developer documentation on tree constructors](../../../developer/trees.md#constructors-and-destructors). - - --- ### Constructor parameters: @@ -417,7 +415,7 @@ mlpack::data::Load("cloud.csv", dataset, true); // // Note that the '<>' isn't necessary if C++20 is being used (e.g. // `mlpack::VPTree tree(...)` will work fine in C++20 or newer). -mlpack::VPTree<> tree(std::move(dataset)); +mlpack::VPTree<> tree(std::move(dataset), 10); // Print the bounding ball of the root node. (There will be no hollow ball.) std::cout << "Bounding ball of root node:" << std::endl; diff --git a/doc/user/core/trees/x_tree.md b/doc/user/core/trees/x_tree.md index ad63df46f3..955317ec63 100644 --- a/doc/user/core/trees/x_tree.md +++ b/doc/user/core/trees/x_tree.md @@ -148,7 +148,7 @@ The dataset is not permuted during the construction process. | **name** | **type** | **description** | **default** | |----------|----------|-----------------|-------------| -| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. Pass with `std::move(data)` to avoid copying the matrix. | _(N/A)_ | +| `data` | [`MatType`](../../matrices.md) | [Column-major](../../matrices.md#representing-data-in-mlpack) matrix to build the tree on. | _(N/A)_ | | `maxLeafSize` | `size_t` | Maximum number of points to store in each leaf. | `20` | | `minLeafSize` | `size_t` | Minimum number of points to store in each leaf. | `8` | | `maxNumChildren` | `size_t` | Maximum number of children allowed in each non-leaf node. | `5` | diff --git a/src/mlpack/core/tree/space_split/hyperplane.hpp b/src/mlpack/core/tree/space_split/hyperplane.hpp index 3e80686e58..c8e682d5ff 100644 --- a/src/mlpack/core/tree/space_split/hyperplane.hpp +++ b/src/mlpack/core/tree/space_split/hyperplane.hpp @@ -25,21 +25,23 @@ namespace mlpack { * @tparam ProjVectorT Type of projection vector (AxisParallelProjVector, * ProjVector). */ -template +template class HyperplaneBase { public: - //! Useful typedef for the bound type. + // Useful typedef for the bound type. using BoundType = BoundT; - //! Useful typedef for the projection vector type. + // Useful typedef for the projection vector type. using ProjVectorType = ProjVectorT; + // Useful typedef for the element type held by data matrices. + using ElemType = typename MatType::elem_type; private: - //! Projection vector. + // Projection vector. ProjVectorType projVect; - //! Projection value that determines the decision boundary. - double splitVal; + // Projection value that determines the decision boundary. + ElemType splitVal; public: /** @@ -55,7 +57,7 @@ class HyperplaneBase * @param projVect Projection vector. * @param splitVal Split value. */ - HyperplaneBase(const ProjVectorType& projVect, double splitVal) : + HyperplaneBase(const ProjVectorType& projVect, ElemType splitVal) : projVect(projVect), splitVal(splitVal) {}; @@ -67,8 +69,9 @@ class HyperplaneBase * @param point Point to be projected. */ template - double Project(const VecType& point, - typename std::enable_if_t::value>* = 0) const + ElemType Project(const VecType& point, + typename std::enable_if_t::value>* = 0) + const { if (splitVal == DBL_MAX) return 0; @@ -139,15 +142,18 @@ class HyperplaneBase /** * AxisOrthogonalHyperplane represents a hyperplane orthogonal to an axis. */ -template -using AxisOrthogonalHyperplane = HyperplaneBase, - AxisParallelProjVector>; +template +using AxisOrthogonalHyperplane = HyperplaneBase< + HRectBound, + AxisParallelProjVector, MatType>; /** * Hyperplane represents a general hyperplane (not necessarily axis-orthogonal). */ -template -using Hyperplane = HyperplaneBase, ProjVector>; +template +using Hyperplane = HyperplaneBase< + BallBound, ProjVector, + MatType>; } // namespace mlpack diff --git a/src/mlpack/core/tree/space_split/mean_space_split_impl.hpp b/src/mlpack/core/tree/space_split/mean_space_split_impl.hpp index 99cd91bd88..f48286872c 100644 --- a/src/mlpack/core/tree/space_split/mean_space_split_impl.hpp +++ b/src/mlpack/core/tree/space_split/mean_space_split_impl.hpp @@ -27,13 +27,13 @@ bool MeanSpaceSplit::SplitSpace( HyperplaneType& hyp) { typename HyperplaneType::ProjVectorType projVector; - double midValue; + typename MatType::elem_type midValue; if (!SpaceSplit::GetProjVector(bound, data, points, projVector, midValue)) return false; - double splitVal = 0.0; + typename MatType::elem_type splitVal = 0.0; for (size_t i = 0; i < points.n_elem; ++i) splitVal += projVector.Project(data.col(points[i])); splitVal /= points.n_elem; diff --git a/src/mlpack/core/tree/space_split/midpoint_space_split_impl.hpp b/src/mlpack/core/tree/space_split/midpoint_space_split_impl.hpp index 9fe8aab3d9..74038c8217 100644 --- a/src/mlpack/core/tree/space_split/midpoint_space_split_impl.hpp +++ b/src/mlpack/core/tree/space_split/midpoint_space_split_impl.hpp @@ -27,7 +27,7 @@ bool MidpointSpaceSplit::SplitSpace( HyperplaneType& hyp) { typename HyperplaneType::ProjVectorType projVector; - double midValue; + typename MatType::elem_type midValue; if (!SpaceSplit::GetProjVector(bound, data, points, projVector, midValue)) diff --git a/src/mlpack/core/tree/space_split/projection_vector.hpp b/src/mlpack/core/tree/space_split/projection_vector.hpp index 8dfc0de532..26fccc4944 100644 --- a/src/mlpack/core/tree/space_split/projection_vector.hpp +++ b/src/mlpack/core/tree/space_split/projection_vector.hpp @@ -88,17 +88,18 @@ class AxisParallelProjVector * ProjVector defines a general projection vector (not necessarily * axis-parallel). */ +template class ProjVector { - //! Projection vector. - arma::vec projVect; + using ProjVecType = typename GetColType::type; + + ProjVecType projVect; public: /** * Empty Constructor. */ - ProjVector() : - projVect() + ProjVector() : projVect() {}; /** @@ -106,7 +107,7 @@ class ProjVector * * @param vect Vector to be considered. */ - ProjVector(const arma::vec& vect) : + ProjVector(const ProjVecType& vect) : projVect(normalise(vect)) {}; diff --git a/src/mlpack/core/tree/space_split/space_split.hpp b/src/mlpack/core/tree/space_split/space_split.hpp index c18c482007..99a75278a7 100644 --- a/src/mlpack/core/tree/space_split/space_split.hpp +++ b/src/mlpack/core/tree/space_split/space_split.hpp @@ -35,11 +35,11 @@ class SpaceSplit * @return Flag to determine if it is possible. */ static bool GetProjVector( - const HRectBound& bound, + const HRectBound& bound, const MatType& data, const arma::Col& points, AxisParallelProjVector& projVector, - double& midValue); + typename MatType::elem_type& midValue); /** * Create a projection vector based on the given set of point. We efficiently @@ -58,8 +58,8 @@ class SpaceSplit const BoundType& bound, const MatType& data, const arma::Col& points, - ProjVector& projVector, - double& midValue); + ProjVector& projVector, + typename MatType::elem_type& midValue); }; } // namespace mlpack diff --git a/src/mlpack/core/tree/space_split/space_split_impl.hpp b/src/mlpack/core/tree/space_split/space_split_impl.hpp index 80793f6498..0978c506bb 100644 --- a/src/mlpack/core/tree/space_split/space_split_impl.hpp +++ b/src/mlpack/core/tree/space_split/space_split_impl.hpp @@ -14,24 +14,27 @@ #define MLPACK_CORE_TREE_SPILL_TREE_SPACE_SPLIT_IMPL_HPP #include "space_split.hpp" +#include namespace mlpack { template bool SpaceSplit::GetProjVector( - const HRectBound& bound, + const HRectBound& bound, const MatType& data, const arma::Col& /* points */, AxisParallelProjVector& projVector, - double& midValue) + typename MatType::elem_type& midValue) { + using ElemType = typename MatType::elem_type; + // Get the dimension that has the maximum width. size_t splitDim = data.n_rows; // Indicate invalid. - double maxWidth = -1; + ElemType maxWidth = -1; for (size_t d = 0; d < data.n_rows; d++) { - const double width = bound[d].Width(); + const ElemType width = bound[d].Width(); if (width > maxWidth) { @@ -56,19 +59,22 @@ bool SpaceSplit::GetProjVector( const BoundType& /* bound */, const MatType& data, const arma::Col& points, - ProjVector& projVector, - double& midValue) + ProjVector& projVector, + typename MatType::elem_type& midValue) { + using ElemType = typename MatType::elem_type; + using VecType = typename GetColType::type; + DistanceType distance; // Efficiently estimate the farthest pair of points in the given set. - size_t fst = points[rand() % points.n_elem]; + size_t fst = points[RandInt(points.n_elem)]; size_t snd = points[0]; - double max = distance.Evaluate(data.col(fst), data.col(snd)); + ElemType max = distance.Evaluate(data.col(fst), data.col(snd)); for (size_t i = 1; i < points.n_elem; ++i) { - double dist = distance.Evaluate(data.col(fst), data.col(points[i])); + ElemType dist = distance.Evaluate(data.col(fst), data.col(points[i])); if (dist > max) { max = dist; @@ -80,7 +86,7 @@ bool SpaceSplit::GetProjVector( for (size_t i = 0; i < points.n_elem; ++i) { - double dist = distance.Evaluate(data.col(fst), data.col(points[i])); + ElemType dist = distance.Evaluate(data.col(fst), data.col(points[i])); if (dist > max) { max = dist; @@ -92,9 +98,9 @@ bool SpaceSplit::GetProjVector( return false; // Calculate the normalized projection vector. - projVector = ProjVector(data.col(snd) - data.col(fst)); + projVector = ProjVector(data.col(snd) - data.col(fst)); - arma::vec midPoint = (data.col(snd) + data.col(fst)) / 2; + VecType midPoint = (data.col(snd) + data.col(fst)) / 2; midValue = projVector.Project(midPoint); diff --git a/src/mlpack/core/tree/spill_tree/is_spill_tree.hpp b/src/mlpack/core/tree/spill_tree/is_spill_tree.hpp index 202769968e..76752fef86 100644 --- a/src/mlpack/core/tree/spill_tree/is_spill_tree.hpp +++ b/src/mlpack/core/tree/spill_tree/is_spill_tree.hpp @@ -26,7 +26,7 @@ struct IsSpillTree template + template class HyperplaneType, template class SplitType> diff --git a/src/mlpack/core/tree/spill_tree/spill_dual_tree_traverser.hpp b/src/mlpack/core/tree/spill_tree/spill_dual_tree_traverser.hpp index 3a4257b8dd..381a1cf89d 100644 --- a/src/mlpack/core/tree/spill_tree/spill_dual_tree_traverser.hpp +++ b/src/mlpack/core/tree/spill_tree/spill_dual_tree_traverser.hpp @@ -27,7 +27,8 @@ namespace mlpack { template class HyperplaneType, + template + class HyperplaneType, template class SplitType> template diff --git a/src/mlpack/core/tree/spill_tree/spill_dual_tree_traverser_impl.hpp b/src/mlpack/core/tree/spill_tree/spill_dual_tree_traverser_impl.hpp index 03ae5ea267..2c951e50af 100644 --- a/src/mlpack/core/tree/spill_tree/spill_dual_tree_traverser_impl.hpp +++ b/src/mlpack/core/tree/spill_tree/spill_dual_tree_traverser_impl.hpp @@ -23,7 +23,8 @@ namespace mlpack { template class HyperplaneType, + template + class HyperplaneType, template class SplitType> template @@ -40,7 +41,8 @@ SpillDualTreeTraverser::SpillDualTreeTraverser( template class HyperplaneType, + template + class HyperplaneType, template class SplitType> template diff --git a/src/mlpack/core/tree/spill_tree/spill_single_tree_traverser.hpp b/src/mlpack/core/tree/spill_tree/spill_single_tree_traverser.hpp index 78969f3d4c..f349dc0a45 100644 --- a/src/mlpack/core/tree/spill_tree/spill_single_tree_traverser.hpp +++ b/src/mlpack/core/tree/spill_tree/spill_single_tree_traverser.hpp @@ -26,7 +26,8 @@ namespace mlpack { template class HyperplaneType, + template + class HyperplaneType, template class SplitType> template diff --git a/src/mlpack/core/tree/spill_tree/spill_single_tree_traverser_impl.hpp b/src/mlpack/core/tree/spill_tree/spill_single_tree_traverser_impl.hpp index f14b52ce3d..eac0f63dbe 100644 --- a/src/mlpack/core/tree/spill_tree/spill_single_tree_traverser_impl.hpp +++ b/src/mlpack/core/tree/spill_tree/spill_single_tree_traverser_impl.hpp @@ -23,7 +23,8 @@ namespace mlpack { template class HyperplaneType, + template + class HyperplaneType, template class SplitType> template @@ -37,7 +38,8 @@ SpillSingleTreeTraverser::SpillSingleTreeTraverser( template class HyperplaneType, + template + class HyperplaneType, template class SplitType> template diff --git a/src/mlpack/core/tree/spill_tree/spill_tree.hpp b/src/mlpack/core/tree/spill_tree/spill_tree.hpp index 052a75c8f3..da71bc5a53 100644 --- a/src/mlpack/core/tree/spill_tree/spill_tree.hpp +++ b/src/mlpack/core/tree/spill_tree/spill_tree.hpp @@ -65,7 +65,7 @@ namespace mlpack { template + template class HyperplaneType = AxisOrthogonalHyperplane, template class SplitType = MidpointSpaceSplit> @@ -77,7 +77,7 @@ class SpillTree //! The type of element held in MatType. using ElemType = typename MatType::elem_type; //! The bound type. - using BoundType = typename HyperplaneType::BoundType; + using BoundType = typename HyperplaneType::BoundType; private: //! The left child node. @@ -95,7 +95,7 @@ class SpillTree //! Flag to distinguish overlapping nodes from non-overlapping nodes. bool overlappingNode; //! Splitting hyperplane represented by this node. - HyperplaneType hyperplane; + HyperplaneType hyperplane; //! The bound object for this node. BoundType bound; //! Any extra data contained in the node. @@ -144,6 +144,13 @@ class SpillTree template using DefeatistDualTreeTraverser = SpillDualTreeTraverser; + /** + * A default constructor. This returns an empty tree, which is not useful. + * In general this is only used for serialization or right before copying from + * a different object. + */ + SpillTree(); + /** * Construct this as the root node of a hybrid spill tree using the given * dataset. The dataset will not be modified during the building procedure @@ -274,7 +281,8 @@ class SpillTree bool Overlap() const { return overlappingNode; } //! Get the Hyperplane instance. - const HyperplaneType& Hyperplane() const { return hyperplane; } + const HyperplaneType& Hyperplane() const + { return hyperplane; } //! Get the distance metric that the tree uses. [[deprecated("Will be removed in mlpack 5.0.0; use Distance()")]] @@ -438,7 +446,7 @@ class SpillTree static bool HasSelfChildren() { return false; } //! Store the center of the bounding region in the given vector. - void Center(arma::vec& center) { bound.Center(center); } + void Center(arma::Col& center) { bound.Center(center); } private: /** @@ -469,17 +477,6 @@ class SpillTree const arma::Col& points, arma::Col& leftPoints, arma::Col& rightPoints); - protected: - /** - * A default constructor. This is meant to only be used with - * cereal, which is allowed with the friend declaration below. - * This does not return a valid tree! The method must be protected, so that - * the serialization shim can work with the default constructor. - */ - SpillTree(); - - //! Friend access is given for the default constructor. - friend class cereal::access; public: /** diff --git a/src/mlpack/core/tree/spill_tree/spill_tree_impl.hpp b/src/mlpack/core/tree/spill_tree/spill_tree_impl.hpp index 44d2c9b357..ed45d45ae8 100644 --- a/src/mlpack/core/tree/spill_tree/spill_tree_impl.hpp +++ b/src/mlpack/core/tree/spill_tree/spill_tree_impl.hpp @@ -18,10 +18,36 @@ namespace mlpack { +// Default constructor (private), for cereal. template class HyperplaneType, + template + class HyperplaneType, + template + class SplitType> +SpillTree:: + SpillTree() : + left(NULL), + right(NULL), + parent(NULL), + count(0), + pointsIndex(NULL), + overlappingNode(false), + stat(*this), + parentDistance(0), + furthestDescendantDistance(0), + dataset(NULL), + localDataset(false) +{ + // Nothing to do. +} + +template + class HyperplaneType, template class SplitType> SpillTree:: @@ -58,7 +84,8 @@ SpillTree( template class HyperplaneType, + template + class HyperplaneType, template class SplitType> SpillTree:: @@ -95,7 +122,8 @@ SpillTree( template class HyperplaneType, + template + class HyperplaneType, template class SplitType> SpillTree:: @@ -130,7 +158,8 @@ SpillTree( template class HyperplaneType, + template + class HyperplaneType, template class SplitType> SpillTree:: @@ -197,7 +226,8 @@ SpillTree(const SpillTree& other) : template class HyperplaneType, + template + class HyperplaneType, template class SplitType> SpillTree& @@ -279,7 +309,8 @@ operator=(const SpillTree& other) template class HyperplaneType, + template + class HyperplaneType, template class SplitType> SpillTree:: @@ -324,7 +355,8 @@ SpillTree(SpillTree&& other) : template class HyperplaneType, + template + class HyperplaneType, template class SplitType> SpillTree& @@ -384,7 +416,8 @@ operator=(SpillTree&& other) template class HyperplaneType, + template + class HyperplaneType, template class SplitType> template @@ -407,7 +440,8 @@ SpillTree( template class HyperplaneType, + template + class HyperplaneType, template class SplitType> SpillTree:: @@ -425,7 +459,8 @@ SpillTree:: template class HyperplaneType, + template + class HyperplaneType, template class SplitType> inline bool SpillTree class HyperplaneType, + template + class HyperplaneType, template class SplitType> inline size_t SpillTree class HyperplaneType, + template + class HyperplaneType, template class SplitType> template @@ -489,7 +526,8 @@ size_t SpillTree class HyperplaneType, + template + class HyperplaneType, template class SplitType> template @@ -515,7 +553,8 @@ size_t SpillTree class HyperplaneType, + template + class HyperplaneType, template class SplitType> size_t SpillTree class HyperplaneType, + template + class HyperplaneType, template class SplitType> size_t SpillTree class HyperplaneType, + template + class HyperplaneType, template class SplitType> inline typename SpillTree:: template class HyperplaneType, + template + class HyperplaneType, template class SplitType> inline typename SpillTree:: template class HyperplaneType, + template + class HyperplaneType, template class SplitType> inline typename SpillTree:: template class HyperplaneType, + template + class HyperplaneType, template class SplitType> inline @@ -642,7 +686,8 @@ SpillTree:: template class HyperplaneType, + template + class HyperplaneType, template class SplitType> inline size_t SpillTree class HyperplaneType, + template + class HyperplaneType, template class SplitType> inline size_t SpillTree class HyperplaneType, + template + class HyperplaneType, template class SplitType> inline size_t SpillTree class HyperplaneType, + template + class HyperplaneType, template class SplitType> inline size_t SpillTree class HyperplaneType, + template + class HyperplaneType, template class SplitType> void @@ -772,7 +821,7 @@ SpillTree:: right = new SpillTree(this, rightPoints, tau, maxLeafSize, rho); // Calculate parent distances for those two nodes. - arma::vec center, leftCenter, rightCenter; + arma::Col center, leftCenter, rightCenter; Center(center); left->Center(leftCenter); right->Center(rightCenter); @@ -789,7 +838,8 @@ SpillTree:: template class HyperplaneType, + template + class HyperplaneType, template class SplitType> bool @@ -872,37 +922,14 @@ SpillTree:: return false; } -// Default constructor (private), for cereal. -template class HyperplaneType, - template - class SplitType> -SpillTree:: - SpillTree() : - left(NULL), - right(NULL), - parent(NULL), - count(0), - pointsIndex(NULL), - overlappingNode(false), - stat(*this), - parentDistance(0), - furthestDescendantDistance(0), - dataset(NULL), - localDataset(false) -{ - // Nothing to do. -} - /** * Serialize the tree. */ template class HyperplaneType, + template + class HyperplaneType, template class SplitType> template diff --git a/src/mlpack/core/tree/spill_tree/traits.hpp b/src/mlpack/core/tree/spill_tree/traits.hpp index a72427da3b..fea5ab3264 100644 --- a/src/mlpack/core/tree/spill_tree/traits.hpp +++ b/src/mlpack/core/tree/spill_tree/traits.hpp @@ -26,7 +26,8 @@ namespace mlpack { template class HyperplaneType, + template + class HyperplaneType, template class SplitType> class TreeTraits +template using SPTree = SpillTree +template using MeanSPTree = SpillTree +template using NonOrtSPTree = SpillTree +template using NonOrtMeanSPTree = SpillTree h1; - AxisOrthogonalHyperplane h2; + Hyperplane h1; + AxisOrthogonalHyperplane h2; arma::mat dataset; dataset.randu(3, 20); // 20 points in 3 dimensions. @@ -40,8 +40,8 @@ TEST_CASE("HyperplaneEmptyConstructor", "[HyperplaneTest]") TEST_CASE("ProjectionTest", "[HyperplaneTest]") { // General hyperplane. - ProjVector projVect1(arma::vec("1 1")); - Hyperplane h1(projVect1, 0); + ProjVector projVect1(arma::vec("1 1")); + Hyperplane h1(projVect1, 0); REQUIRE(h1.Project(arma::vec("1 -1")) == 0); REQUIRE(h1.Left(arma::vec("1 -1"))); @@ -86,7 +86,7 @@ TEST_CASE("AxisOrthogonalProjectionTest", "[HyperplaneTest]") { // AxisParallel hyperplane. AxisParallelProjVector projVect2(1); - AxisOrthogonalHyperplane h2(projVect2, 1); + AxisOrthogonalHyperplane h2(projVect2, 1); REQUIRE(h2.Project(arma::vec("0 0")) == -1); REQUIRE(h2.Left(arma::vec("0 0")));