From 4ccc2cf6bf87395ffd7f64b39ed3da8a71e89c98 Mon Sep 17 00:00:00 2001 From: Ryan Curtin Date: Wed, 2 Oct 2024 10:32:55 -0400 Subject: [PATCH] First steps towards documenting VPTree. --- doc/sidebar.html | 5 + doc/user/core/trees/binary_space_tree.md | 345 ++++++++++++++++++++- src/mlpack/core/tree/hollow_ball_bound.hpp | 10 - 3 files changed, 348 insertions(+), 12 deletions(-) diff --git a/doc/sidebar.html b/doc/sidebar.html index e49272ed4f..d02d5a3047 100644 --- a/doc/sidebar.html +++ b/doc/sidebar.html @@ -86,6 +86,11 @@ when the sidebar is built for each page. MeanSplitKDTree +
  • + + VPTree + +
  • BinarySpaceTree diff --git a/doc/user/core/trees/binary_space_tree.md b/doc/user/core/trees/binary_space_tree.md index de906335aa..1dcb401d2d 100644 --- a/doc/user/core/trees/binary_space_tree.md +++ b/doc/user/core/trees/binary_space_tree.md @@ -421,6 +421,8 @@ write a custom `BoundType` for use with `BinarySpaceTree`: * [`HRectBound`](#hrectbound): hyperrectangle bound, encloses the descendant points in the smallest possible hyperrectangle + * [`HollowBallBound`](#hollowballbound): hollow ball bound, equivalent to a + ball bound with a ball subtracted from it. * [Custom `BoundType`s](#custom-boundtypes): implement a fully custom `BoundType` @@ -567,7 +569,7 @@ operations with data points or other bounds. Once an `HRectBound` has been successfully created and set to the desired bounding hyperrectangle, there are a number of functions that can bound the -distance between a `HRectBound` and other objects. +distance between an `HRectBound` and other objects. * `b.Contains(point)` * `b.Contains(bound)` @@ -764,13 +766,352 @@ std::cout << "Distance between Manhattan distance HRectBound and " // point. arma::fmat floatData(3, 25, arma::fill::randu); mlpack::HRectBound cb; -cb |= floatData; // This will set the bound to [2.0, 3.0] in every dimension. +cb |= floatData; // Note the use of arma::fvec to represent a point, since ElemType is float. const mlpack::RangeType r3 = cb.RangeDistance(arma::fvec("1.5 1.5 4.0")); std::cout << "Distance between Chebyshev distance HRectBound and " << "[1.5, 1.5, 4.0]: [" << r3.Lo() << ", " << r3.Hi() << "]." << std::endl; ``` +### `HollowBallBound` + +The `HollowBallBound` class represents a bounding shape that is an +arbitrary-dimensional ball bound with another smaller ball subtracted from its +inside. A `HollowBallBound` consists of a center point, an outer radius, and a +secondary center point and inner radius. An example `HollowBallBound` is shown +below in two dimensions; shaded area represents area held within the bound. + +
    +hollow ball bound +
    + +`HollowBallBound` is used directly by the [`VPTree`](vptree.md) class. + +#### Constructors + +`HollowBallBound` allows configurable behavior via its two template parameters: + +``` +HollowBallBound +``` + +Different constructor forms can be used to specify different template parameters +(and thus different bound behavior). + + * `b = HollowBallBound(dimensionality)` + - Construct a `HollowBallBound` with the given `dimensionality`. + - The bound will be empty with invalid centers and radii (e.g., `b` will not + contain any points at all). + - The bound will use the [Euclidean distance](../distances.md#lmetric) for + distance computation, and will expect data to have elements with type + `double`. + + * `b = HollowBallBound(dimensionality)` + - Construct a `HollowBallBound` with the given `dimensionality` that will use + the given `DistanceType` class to compute distances, and expect data to + have elements with type `ElemType`. + - `ElemType` should generally be `double` or `float`. + + + +***Note***: these constructors provide an empty bound; be sure to +[grow](#growing-and-shrinking-the-bound-1) the bound or +[directly modify the bound](#accessing-and-modifying-properties-of-the-bound-1) +before using it! + +--- + + * `b = HollowBallBound(innerRadius, outerRadius, center)` + - Construct a `HollowBallBound` with the given `innerRadius` for the inner + ball, `outerRadius` for the outer ball, and `center`. + - Both the inner and outer ball are centered at `center`. + - `innerRadius` and `outerRadius` should have type `double`. + - `center` should have type `arma::vec`. + - The bound will use the [Euclidean distance](../distances.md#lmetric) for + distance computation, and will expect data to have elements with type + `double`. + + * `b = HollowBallBound(innerRadius, outerRadius, center)` + - Construct a `HollowBallBound` with the given `innerRadius` for the inner + ball, `outerRadius` for the outer ball, and `center`. + - Both the inner and outer ball are centered at `center`. + - `innerRadius` and `outerRadius` should have type `ElemType`. + - `center` should be a vector with element type `ElemType` (e.g. + `arma::Col`). + - The bound will use the given `DistanceType` class to compute distances, and + expect data to have elements with type `ElemType`. + +#### Accessing and modifying properties of the bound + +The individual bounds associated with each dimension of a `HollowBallBound` can +be accessed and modified. + + * `b.Dim()` will return a `size_t` indicating the dimensionality of the bound. + + * `b.Center()` returns an `arma::vec&` containing the center of the outer ball. + Its elements can be directly modified. + + * `b.HollowCenter()` returns an `arma::vec&` containing the center of the inner + ball. Its elements can be directly modified. + + * `b.OuterRadius()` will return a `double` that is the radius of the outer + ball. + - `b.OuterRadius() = r` will set the radius of the outer ball to `r`. + + * `b.InnerRadius()` will return a `double` that is the radius of the inner + ball. + - `b.InnerRadius() = r` will set the radius of the inner ball to `r`. + + * `b[dim]` will return a [`Range`](../math.md#range) object representing the + extents of the bound in dimension `dim`. + - The range is defined as + `[b.Center()[dim] - b.OuterRadius(), b.Center()[dim] + b.OuterRadius()]`. + - ***Note:*** this returns the maximum extents of the bound and does not + consider the inner (hollow) ball. + + * `b.Diameter()` returns the diameter of the ball. This is always equal to + `2 * b.OuterRadius()`. + + * `b.MinWidth()` returns the minimum width of the bound in any dimension as a + `double`. This is always equal to `b.Diameter()`. + + * `b.Distance()` returns either a + [`EuclideanDistance`](../distances.md#lmetric) distance metric object, or a + `DistanceType` if a custom `DistanceType` has been specified in the + constructor. + + * `b.Center(center)` will store the center of the `HollowBallBound` in the + vector `center`. `center` should be of type `arma::vec`. + + * `b.MinWidth()` returns the minimum width of the bound in any dimension as a + `double`. This value is cached and no computation is performed when calling + `b.MinWidth()`. If the bound is empty, `0` is returned. + + * `b.Distance()` returns either a + [`EuclideanDistance`](../distances.md#lmetric) distance metric object, or a + `DistanceType` if a custom `DistanceType` has been specified in the + constructor. + + * `b.Center(center)` will compute the center of the `HRectBound` (e.g. the + vector with elements equal to the midpoint of `b` in each dimension) and + store it in the vector `center`. `center` should be of type `arma::vec`. + + * `b.Volume()` computes the volume of the hyperrectangle specified by `b`. The + volume is returned as a `double`. + + * `b.Diameter()` computes the longest diagonal of the hyperrectangle specified + by `b`. + + * A `HollowBallBound` can be serialized with + [`data::Save()` and `data::Load()`](../../load_save.md#mlpack-objects). + +***Note:*** if a custom `ElemType` was specified in the constructor, then: + + * `b[dim]` will return a `RangeType`; + * `b.OuterRadius()`, `b.InnerRadius()`, `b.MinWidth()`, and `b.Diameter()` will + return `ElemType`; + * `b.Center()` and `b.HollowCenter()` will return `arma::Col&`; and + * `b.Center(center)` expects `center` to be of type `arma::Col`. + +#### Growing the bound + +The `HollowBallBound` uses the logical `|=` to grow the bound to include points. + + * `b |= data` expands `b` so the outer ball includes all of the data points in + `data`, shrinking the inner ball as necessary. `data` should be a + [column-major `arma::mat`](../../matrices.md#representing-data-in-mlpack). + The expansion operation is minimal, so `b` is not expanded any more than + necessary. + - The bound is grown using [Jack Ritter's bounding sphere + algorithm](https://en.wikipedia.org/wiki/Bounding_sphere#Ritter's_bounding_sphere), + which may move the center of the bound as it iteratively adds points to the + bound. (The hollow center is not moved.) + - If the bound is empty, the centers are initialized to the first point of + `data`. + - If the bound is not empty, then `data` is expected to have dimensionality + that matches `b.Dim()`. + +***Notes:*** + + - The growth operation does not grow the inner (hollow) ball. Properties + related to the inner ball should be set manually with `b.HollowCenter()` and + `b.InnerRadius()`. + + - If a custom `ElemType` was specified, then any `data` argument should be a + matrix with that `ElemType` (e.g. `arma::Mat`). + +#### Bounding distances to other objects + +Once a `HollowBallBound` has been successfully created and set to the desired +bounding balls, there are a number of functions that can bound the +distance between a `HollowBallBound` and other objects. + + * `b.Contains(point)` + * `b.Contains(bound)` + - Return a `bool` indicating whether or not `b` contains the given `point` + (an `arma::vec`) or another `bound` (an `HRectBound`). + - When passing another `bound`, `true` will be returned if `bound` even + partially overlaps with `b`. + + * `b.MinDistance(point)` + * `b.MinDistance(bound)` + - Return a `double` whose value is the minimum possible distance between `b` + and either a `point` (an `arma::vec`) or another `bound` (a + `HollowBallBound`). + - The minimum distance between `b` and another point or bound is the length + of the shortest possible line that can connect the other point or bound to + `b`. + - If `point` or `bound` are contained in `b`, then the returned distance is + 0. + + * `b.MaxDistance(point)` + * `b.MaxDistance(bound)` + - Return a `double` whose value is the maximum possible distance between `b` + and either a `point` (an `arma::vec`) or another `bound` (a + `HollowBallBound`). + - The maximum distance between `b` and a given `point` is the furthest + possible distance between `point` and any possible point falling within the + bounding hyperrectangle of `b`. + - The maximum distance between `b` and another `bound` is the furthest + possible distance between any possible point falling within the bounding + hyperrectangle of `b`, and any possible point falling within the bounding + hyperrectangle of `bound`. + - Note that this definition means that even if `b.Contains(point)` or + `b.Contains(bound)` is `true`, the maximum distance may be greater than + `0`. + + * `b.RangeDistance(point)` + * `b.RangeDistance(bound)` + - Compute the minimum and maximum distance between `b` and `point` or + `bound`, returning the result as a [`Range`](../math.md#range) object. + - This is more efficient than calling `b.MinDistance()` and + `b.MaxDistance()`. + +***Note:*** if a custom `DistanceType` and `ElemType` were specified in the +constructor, then all distances will be computed with respect to the specified +`DistanceType` and all return values will either be `ElemType` or +[`RangeType`](../math.md#range) (except for `Contains()`, which will +still return a `bool`). + +#### Example usage + +```c++ +// Create a hollow ball bound in 3 dimensions whose outer ball is the unit ball +// and whose inner ball is the ball with radius 0.5 centered at the origin. +// The bounding range for all three dimensions is [0.0, 1.0]. +mlpack::HollowBallBound b(0.5, 1.0, arma::vec(3)); + +std::cout << "Hollow unit ball bound created manually:" << std::endl; +std::cout << " - Center: " << b.Center().t(); +std::cout << " - Outer radius: " << b.OuterRadius() << "." << std::endl; +std::cout << " - Hollow center: " << b.HollowCenter.(); +std::cout << " - Inner radius: " << b.InnerRadius() << "." << std::endl; +for (size_t i = 0; i < 3; ++i) +{ + std::cout << " - Dimension " << i << " extents: [" << b[i].Lo() << ", " + << b[i].Hi() << "]." << std::endl; +} +std::cout << std::endl; + +// Create a small dataset of 5 points. +arma::mat dataset(3, 5); +dataset.col(0) = arma::vec("2.0 2.0 2.0"); +dataset.col(1) = arma::vec("2.5 2.5 2.5"); +dataset.col(2) = arma::vec("3.0 2.0 3.0"); +dataset.col(3) = arma::vec("2.0 3.0 2.0"); +dataset.col(4) = arma::vec("3.0 3.0 3.0"); + +// If we simply build a HollowBallBound to enclose those points, the hollow part +// of the ball is unmodified and remains empty. +mlpack::HollowBallBound b2(3); +b2 |= dataset; +std::cout << "Hollow ball bound on points with only `operator|=()`:" + << std::endl; +std::cout << " - Center: " << b2.Center().t(); +std::cout << " - Outer radius: " << b2.OuterRadius() << "." << std::endl; +std::cout << " - Hollow center: " << b2.HollowCenter().t(); +std::cout << " - Inner radius: " << b2.InnerRadius() << "." << std::endl; +std::cout << std::endl; + +// On the other hand, if we initialize a HollowBallBound to a non-empty bound, +// then `operator|=()` will shrink the hollow ball as necessary. +// +// We initialize this ball bound to a "slice" with radii [2.6, 2.7]. +mlpack::HollowBallBound b3(2.6, 2.7, arma::vec(3)); +b3 |= dataset; +std::cout << "Hollow ball bound on points with pre-initialization and " + << "`operator|=()`:" << std::endl; +std::cout << " - Center: " << b3.Center().t(); +std::cout << " - Outer radius: " << b3.OuterRadius() << "." << std::endl; +std::cout << " - Hollow center: " << b3.HollowCenter().t(); +std::cout << " - Inner radius: " << b3.InnerRadius() << "." << std::endl; +std::cout << std::endl; + +// Manually create a hollow ball bound whose hollow center is different than the +// outer ball's center. +mlpack::HollowBallBound b4(3); +b4.OuterRadius() = 3.0; +b4.InnerRadius() = 1.5; +b4.Center() = arma::vec(3); +b4.HollowCenter() = arma::vec("1.0 1.0 1.0"); + +// Compute the minimum distance between a point inside the hollow unit ball's +// outer ball. +const double d1 = b.MinDistance(arma::vec("0.75 0.75 0.75")); +std::cout << "Minimum distance between hollow unit ball bound and [0.75, 0.75, " + << "0.75]: " << d1 << "." << std::endl; + +// Compute the minimum distance between a point inside the hollow unit ball's +// inner ball (so the point is not contained in the bound---it is within the +// hollow section). +const double d2 = b.MinDistance(arma::vec("0.25 0.25 0.25")); +std::cout << "Minimum distance between hollow unit ball bound and [0.25, 0.25, " + << "0.25]: " << d2 << "." << std::endl; +std::cout << std::endl; + +// Use Contains(). In this case, the 'else' will be taken. +if (b.Contains(arma::vec("1.5 1.5 1.5"))) +{ + std::cout << "Hollow unit ball bound contains [1.5, 1.5, 1.5]." << std::endl; +} +else +{ + std::cout << "Hollow unit ball bound does not contain [1.5, 1.5, 1.5]." + << std::endl; +} +std::cout << std::endl; + +// Compute the maximum distance between a point inside the unit ball and the +// unit hollow ball bound. +const double d2 = b4.MaxDistance(arma::vec("0.1 0.1 0.1")); +std::cout << "Maximum distance between hollow unit ball bound and [0.1, 0.1, " + << "0.1]: " << d2 << "." << std::endl; + +// Compute the minimum and maximum distances between the hollow unit ball bound +// and the bound built on data points. +const mlpack::Range r = b.RangeDistance(b3); +std::cout << "Distances between hollow unit ball bound and second hollow " + << "dataset bound: [" << r.Lo() << ", " << r.Hi() << "]." << std::endl; + +// Create a bound using the Manhattan (L1) distance and compute the minimum and +// maximum distance to a point. +mlpack::HollowBallBound mb(2.0, 5.0, arma::vec(3)); +const mlpack::Range r2 = mb.RangeDistance(arma::vec("1.5 1.5 4.0")); +std::cout << "Distance between Manhattan distance HollowBallBound and " + << "[1.5, 1.5, 4.0]: [" << r2.Lo() << ", " << r2.Hi() << "]." << std::endl; + +// Create a bound using the Chebyshev (L-inf) distance, using random 32-bit +// floating point elements, and compute the minimum and maximum distance to a +// point. +arma::fmat floatData(3, 25, arma::fill::randu); +mlpack::HollowBallBound cb; +cb |= floatData; +// Note the use of arma::fvec to represent a point, since ElemType is float. +const mlpack::RangeType r3 = cb.RangeDistance(arma::fvec("1.5 1.5 4.0")); +std::cout << "Distance between Chebyshev distance HollowBallBound and " + << "[1.5, 1.5, 4.0]: [" << r3.Lo() << ", " << r3.Hi() << "]." << std::endl; +``` + ### Custom `BoundType`s The `BinarySpaceTree` class allows an arbitrary `BoundType` template parameter diff --git a/src/mlpack/core/tree/hollow_ball_bound.hpp b/src/mlpack/core/tree/hollow_ball_bound.hpp index 2cad206b82..a41c10798f 100644 --- a/src/mlpack/core/tree/hollow_ball_bound.hpp +++ b/src/mlpack/core/tree/hollow_ball_bound.hpp @@ -212,16 +212,6 @@ class HollowBallBound template const HollowBallBound& operator|=(const MatType& data); - /** - * Expand the bound to include the given bound. The centroid will not be - * moved. - * - * @tparam MatType Type of matrix; could be arma::mat, arma::spmat, or a - * vector. - * @tparam data Data points to add. - */ - const HollowBallBound& operator|=(const HollowBallBound& other); - /** * Returns the diameter of the ballbound. */