From da47b6ea248e1ecf9d1c8cd35dc2d6d882575242 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 23 Apr 2015 14:41:41 -0400 Subject: [PATCH] Refactor RASearch to take queries in Search(). --- src/mlpack/methods/rann/allkrann_main.cpp | 199 +++---- src/mlpack/methods/rann/ra_search.hpp | 384 +++++++------ src/mlpack/methods/rann/ra_search_impl.hpp | 529 ++++++++++-------- src/mlpack/methods/rann/ra_search_rules.hpp | 8 +- .../methods/rann/ra_search_rules_impl.hpp | 22 +- 5 files changed, 594 insertions(+), 548 deletions(-) diff --git a/src/mlpack/methods/rann/allkrann_main.cpp b/src/mlpack/methods/rann/allkrann_main.cpp index 6ed52cc264..eb0ab877d5 100644 --- a/src/mlpack/methods/rann/allkrann_main.cpp +++ b/src/mlpack/methods/rann/allkrann_main.cpp @@ -8,13 +8,13 @@ #include #include -#include #include #include #include #include "ra_search.hpp" +#include using namespace std; using namespace mlpack; @@ -68,8 +68,6 @@ PARAM_FLAG("naive", "If true, sampling will be done without using a tree.", "N"); PARAM_FLAG("single_mode", "If true, single-tree search is used (as opposed to " "dual-tree search.", "s"); -PARAM_FLAG("cover_tree", "If true, use cover trees to perform the search.", - "c"); PARAM_FLAG("sample_at_leaves", "The flag to trigger sampling at leaves.", "L"); PARAM_FLAG("first_leaf_exact", "The flag to trigger sampling only after " @@ -117,6 +115,15 @@ int main(int argc, char *argv[]) Log::Fatal << referenceData.n_cols << ")." << endl; } + // Load query data, if necessary. + if (CLI::HasParam("query_file")) + { + const string queryFile = CLI::GetParam("query_file"); + data::Load(queryFile, queryData, true); + Log::Info << "Loaded query data from '" << queryFile << "' (" + << queryData.n_rows << " x " << queryData.n_cols << ")." << endl; + } + // Sanity check on the value of 'tau' with respect to 'k' so that // 'k' neighbors are not requested from the top-'rank_error' neighbors // where 'rank_error' <= 'k'. @@ -142,152 +149,82 @@ int main(int argc, char *argv[]) if (naive) { - AllkRANN* allkrann; + AllkRANN allkrann(referenceData, naive, false, tau, alpha); + + Log::Info << "Computing " << k << " nearest neighbors " << "with " + << tau << "% rank approximation..." << endl; + if (CLI::GetParam("query_file") != "") - { - string queryFile = CLI::GetParam("query_file"); - - data::Load(queryFile, queryData, true); - - Log::Info << "Loaded query data from '" << queryFile << "' (" << - queryData.n_rows << " x " << queryData.n_cols << ")." << endl; - - allkrann = new AllkRANN(referenceData, queryData, naive); - } + allkrann.Search(queryData, k, neighbors, distances); else - allkrann = new AllkRANN(referenceData, naive); - - Log::Info << "Computing " << k << " nearest neighbors " << "with " << - tau << "% rank approximation..." << endl; - - allkrann->Search(k, neighbors, distances, tau, alpha); + allkrann.Search(k, neighbors, distances); Log::Info << "Neighbors computed." << endl; - - delete allkrann; } else { - // The results output by the AllkRANN class - // shuffled because the tree construction shuffles the point sets. + // The results output by the AllkRANN class are + // shuffled if the tree construction shuffles the point sets. arma::Mat neighborsOut; arma::mat distancesOut; - if (!CLI::HasParam("cover_tree")) + // Mappings for when we build the tree. + std::vector oldFromNewRefs; + std::vector oldFromNewQueries; + + // Build trees by hand, so we can save memory: if we pass a tree to + // NeighborSearch, it does not copy the matrix. + Log::Info << "Building reference tree..." << endl; + Timer::Start("tree_building"); + typedef BinarySpaceTree, + RAQueryStat > TreeType; + TreeType refTree(referenceData, oldFromNewRefs, leafSize); + Timer::Stop("tree_building"); + + // Because we may construct it differently, we need a pointer. + AllkRANN allkrann(&refTree, singleMode, tau, alpha, sampleAtLeaves, + firstLeafExact, singleSampleLimit); + + if (CLI::HasParam("query_file") && !singleMode) { - // Because we may construct it differently, we need a pointer. - AllkRANN* allkrann = NULL; - - // Mappings for when we build the tree. - std::vector oldFromNewRefs; - - // Build trees by hand, so we can save memory: if we pass a tree to - // NeighborSearch, it does not copy the matrix. - Log::Info << "Building reference tree..." << endl; + Log::Info << "Building query tree..." << endl; Timer::Start("tree_building"); - - BinarySpaceTree, - RAQueryStat > - refTree(referenceData, oldFromNewRefs, leafSize); - BinarySpaceTree, - RAQueryStat >* - queryTree = NULL; // Empty for now. - + TreeType queryTree(queryData, oldFromNewQueries, leafSize); Timer::Stop("tree_building"); - - std::vector oldFromNewQueries; - - if (CLI::GetParam("query_file") != "") - { - string queryFile = CLI::GetParam("query_file"); - - data::Load(queryFile, queryData, true); - - if (naive && leafSize < queryData.n_cols) - leafSize = queryData.n_cols; - - Log::Info << "Loaded query data from '" << queryFile << "' (" << - queryData.n_rows << " x " << queryData.n_cols << ")." << endl; - - Log::Info << "Building query tree..." << endl; - - // Build trees by hand, so we can save memory: if we pass a tree to - // NeighborSearch, it does not copy the matrix. - Timer::Start("tree_building"); - - queryTree = new BinarySpaceTree, - RAQueryStat > - (queryData, oldFromNewQueries, leafSize); - Timer::Stop("tree_building"); - - allkrann = new AllkRANN(&refTree, queryTree, referenceData, queryData, - singleMode); - - Log::Info << "Tree built." << endl; - } - else - { - allkrann = new AllkRANN(&refTree, referenceData, singleMode); - Log::Info << "Trees built." << endl; - } + Log::Info << "Tree built." << endl; Log::Info << "Computing " << k << " nearest neighbors " << "with " << - tau << "% rank approximation..." << endl; - allkrann->Search(k, neighborsOut, distancesOut, - tau, alpha, sampleAtLeaves, - firstLeafExact, singleSampleLimit); - - Log::Info << "Neighbors computed." << endl; - - // We have to map back to the original indices from before the tree - // construction. - Log::Info << "Re-mapping indices..." << endl; - - neighbors.set_size(neighborsOut.n_rows, neighborsOut.n_cols); - distances.set_size(distancesOut.n_rows, distancesOut.n_cols); - - // Do the actual remapping. - if (CLI::GetParam("query_file") != "") - { - for (size_t i = 0; i < distancesOut.n_cols; ++i) - { - // Map distances (copy a column). - distances.col(oldFromNewQueries[i]) = distancesOut.col(i); - - // Map indices of neighbors. - for (size_t j = 0; j < distancesOut.n_rows; ++j) - { - neighbors(j, oldFromNewQueries[i]) - = oldFromNewRefs[neighborsOut(j, i)]; - } - } - } - else - { - for (size_t i = 0; i < distancesOut.n_cols; ++i) - { - // Map distances (copy a column). - distances.col(oldFromNewRefs[i]) = distancesOut.col(i); - - // Map indices of neighbors. - for (size_t j = 0; j < distancesOut.n_rows; ++j) - { - neighbors(j, oldFromNewRefs[i]) - = oldFromNewRefs[neighborsOut(j, i)]; - } - } - } - - // Clean up. - if (queryTree) - delete queryTree; - - delete allkrann; + tau << "% rank approximation..." << endl; + allkrann.Search(&queryTree, k, neighborsOut, distancesOut); } - else // Cover trees. + else if (CLI::HasParam("query_file") && singleMode) { - Log::Fatal << "Cover tree case not implemented yet..." << endl; + Log::Info << "Computing " << k << " nearest neighbors " << "with " << + tau << "% rank approximation..." << endl; + allkrann.Search(queryData, k, neighborsOut, distancesOut); } + else + { + Log::Info << "Computing " << k << " nearest neighbors " << "with " << + tau << "% rank approximation..." << endl; + allkrann.Search(k, neighborsOut, distancesOut); + } + + Log::Info << "Neighbors computed." << endl; + + // We have to map back to the original indices from before the tree + // construction. + Log::Info << "Re-mapping indices..." << endl; + + // Map the results back to the correct places. + if ((CLI::GetParam("query_file") != "") && !singleMode) + Unmap(neighborsOut, distancesOut, oldFromNewRefs, oldFromNewQueries, + neighbors, distances); + else if ((CLI::GetParam("query_file") != "") && singleMode) + Unmap(neighborsOut, distancesOut, oldFromNewRefs, neighbors, distances); + else + Unmap(neighborsOut, distancesOut, oldFromNewRefs, oldFromNewRefs, + neighbors, distances); } // Save output. diff --git a/src/mlpack/methods/rann/ra_search.hpp b/src/mlpack/methods/rann/ra_search.hpp index ae2cfe063d..135f381e72 100644 --- a/src/mlpack/methods/rann/ra_search.hpp +++ b/src/mlpack/methods/rann/ra_search.hpp @@ -57,158 +57,38 @@ class RASearch { public: /** - * Initialize the RASearch object, passing both a query and reference dataset. - * Optionally, perform the computation in naive mode or single-tree mode, and - * set the leaf size used for tree-building. An initialized distance metric - * can be given, for cases where the metric has internal data (i.e. the + * Initialize the RASearch object, passing both a reference dataset (this is + * the dataset that will be searched). Optionally, perform the computation in + * naive mode or single-tree mode. An initialized distance metric can be + * given, for cases where the metric has internal data (i.e. the * distance::MahalanobisDistance class). * * This method will copy the matrices to internal copies, which are rearranged * during tree-building. You can avoid this extra copy by pre-constructing - * the trees and passing them using a diferent constructor. + * the trees and passing them using a different constructor. + * + * tau, the rank-approximation parameter, specifies that we are looking for k + * neighbors with probability alpha of being in the top tau percent of nearest + * neighbors. So, as an example, if our dataset has 1000 points, and we want + * 5 nearest neighbors with 95% probability of being in the top 5% of nearest + * neighbors (or, the top 50 nearest neighbors), we set k = 5, tau = 5, and + * alpha = 0.95. + * + * The method will fail (and throw a std::invalid_argument exception) if the + * value of tau is too low: tau must be set such that the number of points in + * the corresponding percentile of the data is greater than k. Thus, if we + * choose tau = 0.1 with a dataset of 1000 points and k = 5, then we are + * attempting to choose 5 nearest neighbors out of the closest 1 point -- this + * is invalid. * * @param referenceSet Set of reference points. - * @param querySet Set of query points. * @param naive If true, the rank-approximate search will be performed by * directly sampling the whole set instead of using the stratified * sampling on the tree. * @param singleMode If true, single-tree search will be used (as opposed to - * dual-tree search). - * @param leafSize Leaf size for tree construction (ignored if tree is given). + * dual-tree search). This is useful when Search() will be called with + * few query points. * @param metric An optional instance of the MetricType class. - */ - RASearch(const typename TreeType::Mat& referenceSet, - const typename TreeType::Mat& querySet, - const bool naive = false, - const bool singleMode = false, - const MetricType metric = MetricType()); - - /** - * Initialize the RASearch object, passing only one dataset, which is - * used as both the query and the reference dataset. Optionally, perform the - * computation in naive mode or single-tree mode, and set the leaf size used - * for tree-building. An initialized distance metric can be given, for cases - * where the metric has internal data (i.e. the distance::MahalanobisDistance - * class). - * - * If naive mode is being used and a pre-built tree is given, it may not work: - * naive mode operates by building a one-node tree (the root node holds all - * the points). If that condition is not satisfied with the pre-built tree, - * then naive mode will not work. - * - * @param referenceSet Set of reference points. - * @param naive If true, the rank-approximate search will be performed - * by directly sampling the whole set instead of using the stratified - * sampling on the tree. - * @param singleMode If true, single-tree search will be used (as opposed to - * dual-tree search). - * @param leafSize Leaf size for tree construction (ignored if tree is given). - * @param metric An optional instance of the MetricType class. - */ - RASearch(const typename TreeType::Mat& referenceSet, - const bool naive = false, - const bool singleMode = false, - const MetricType metric = MetricType()); - - /** - * Initialize the RASearch object with the given datasets and - * pre-constructed trees. It is assumed that the points in referenceSet and - * querySet correspond to the points in referenceTree and queryTree, - * respectively. Optionally, choose to use single-tree mode. Naive mode is - * not available as an option for this constructor; instead, to run naive - * computation, construct a tree with all of the points in one leaf (i.e. - * leafSize = number of points). Additionally, an instantiated distance - * metric can be given, for cases where the distance metric holds data. - * - * There is no copying of the data matrices in this constructor (because - * tree-building is not necessary), so this is the constructor to use when - * copies absolutely must be avoided. - * - * @note - * Because tree-building (at least with BinarySpaceTree) modifies the ordering - * of a matrix, be sure you pass the modified matrix to this object! In - * addition, mapping the points of the matrix back to their original indices - * is not done when this constructor is used. - * @endnote - * - * @param referenceTree Pre-built tree for reference points. - * @param queryTree Pre-built tree for query points. - * @param referenceSet Set of reference points corresponding to referenceTree. - * @param querySet Set of query points corresponding to queryTree. - * @param singleMode Whether single-tree computation should be used (as - * opposed to dual-tree computation). - * @param metric Instantiated distance metric. - */ - RASearch(TreeType* referenceTree, - TreeType* queryTree, - const typename TreeType::Mat& referenceSet, - const typename TreeType::Mat& querySet, - const bool singleMode = false, - const MetricType metric = MetricType()); - - /** - * Initialize the RASearch object with the given reference dataset and - * pre-constructed tree. It is assumed that the points in referenceSet - * correspond to the points in referenceTree. Optionally, choose to use - * single-tree mode. Naive mode is not available as an option for this - * constructor; instead, to run naive computation, construct a tree with all - * the points in one leaf (i.e. leafSize = number of points). Additionally, - * an instantiated distance metric can be given, for the case where the - * distance metric holds data. - * - * There is no copying of the data matrices in this constructor (because - * tree-building is not necessary), so this is the constructor to use when - * copies absolutely must be avoided. - * - * @note - * Because tree-building (at least with BinarySpaceTree) modifies the ordering - * of a matrix, be sure you pass the modified matrix to this object! In - * addition, mapping the points of the matrix back to their original indices - * is not done when this constructor is used. - * @endnote - * - * @param referenceTree Pre-built tree for reference points. - * @param referenceSet Set of reference points corresponding to referenceTree. - * @param singleMode Whether single-tree computation should be used (as - * opposed to dual-tree computation). - * @param metric Instantiated distance metric. - */ - RASearch(TreeType* referenceTree, - const typename TreeType::Mat& referenceSet, - const bool singleMode = false, - const MetricType metric = MetricType()); - - /** - * Delete the RASearch object. The tree is the only member we are - * responsible for deleting. The others will take care of themselves. - */ - ~RASearch(); - - /** - * Compute the rank approximate nearest neighbors and store the output in the - * given matrices. The matrices will be set to the size of n columns by k - * rows, where n is the number of points in the query dataset and k is the - * number of neighbors being searched for. - * - * Note that tau, the rank-approximation parameter, specifies that we are - * looking for k neighbors with probability alpha of being in the top tau - * percent of nearest neighbors. So, as an example, if our dataset has 1000 - * points, and we want 5 nearest neighbors with 95% probability of being in - * the top 5% of nearest neighbors (or, the top 50 nearest neighbors), we set - * k = 5, tau = 5, and alpha = 0.95. - * - * The method will fail (and issue a failure message) if the value of tau is - * too low: tau must be set such that the number of points in the - * corresponding percentile of the data is greater than k. Thus, if we choose - * tau = 0.1 with a dataset of 1000 points and k = 5, then we are attempting - * to choose 5 nearest neighbors out of the closest 1 point -- this is - * invalid. - * - * @param k Number of neighbors to search for. - * @param resultingNeighbors Matrix storing lists of neighbors for each query - * point. - * @param distances Matrix storing distances of neighbors for each query - * point. * @param tau The rank-approximation in percentile of the data. The default * value is 5%. * @param alpha The desired success probability. The default value is 0.95. @@ -220,70 +100,222 @@ class RASearch * @param singleSampleLimit The limit on the largest node that can be * approximated by sampling. This defaults to 20. */ - void Search(const size_t k, - arma::Mat& resultingNeighbors, - arma::mat& distances, - const double tau = 5, - const double alpha = 0.95, - const bool sampleAtLeaves = false, - const bool firstLeafExact = false, - const size_t singleSampleLimit = 20); + RASearch(const typename TreeType::Mat& referenceSet, + const bool naive = false, + const bool singleMode = false, + const double tau = 5, + const double alpha = 0.95, + const bool sampleAtLeaves = false, + const bool firstLeafExact = false, + const size_t singleSampleLimit = 20, + const MetricType metric = MetricType()); /** - * This function recursively resets the RAQueryStat of the queryTree to set - * 'bound' to WorstDistance and the 'numSamplesMade' to 0. This allows a user - * to perform multiple searches on the same pair of trees, possibly with - * different levels of approximation without requiring to build a new pair of - * trees for every new (approximate) search. + * Initialize the RASearch object with the given pre-constructed reference + * tree. It is assumed that the points in the tree's dataset correspond to + * the reference set. Optionally, choose to use single-tree mode. Naive mode + * is not available as an option for this constructor; instead, to run naive + * computation, use a different constructor. Additionally, an instantiated + * distance metric can be given, for cases where the distance metric holds + * data. + * + * There is no copying of the data matrices in this constructor (because + * tree-building is not necessary), so this is the constructor to use when + * copies absolutely must be avoided. + * + * tau, the rank-approximation parameter, specifies that we are looking for k + * neighbors with probability alpha of being in the top tau percent of nearest + * neighbors. So, as an example, if our dataset has 1000 points, and we want + * 5 nearest neighbors with 95% probability of being in the top 5% of nearest + * neighbors (or, the top 50 nearest neighbors), we set k = 5, tau = 5, and + * alpha = 0.95. + * + * The method will fail (and throw a std::invalid_argument exception) if the + * value of tau is too low: tau must be set such that the number of points in + * the corresponding percentile of the data is greater than k. Thus, if we + * choose tau = 0.1 with a dataset of 1000 points and k = 5, then we are + * attempting to choose 5 nearest neighbors out of the closest 1 point -- this + * is invalid. + * + * @note + * Tree-building may (at least with BinarySpaceTree) modify the ordering + * of a matrix, so be aware that the results you get from Search() will + * correspond to the modified matrix. + * @endnote + * + * @param referenceTree Pre-built tree for reference points. + * @param singleMode Whether single-tree computation should be used (as + * opposed to dual-tree computation). + * @param metric Instantiated distance metric. + * @param tau The rank-approximation in percentile of the data. The default + * value is 5%. + * @param alpha The desired success probability. The default value is 0.95. + * @param sampleAtLeaves Sample at leaves for faster but less accurate + * computation. This defaults to 'false'. + * @param firstLeafExact Traverse to the first leaf without approximation. + * This can ensure that the query definitely finds its (near) duplicate + * if there exists one. This defaults to 'false' for now. + * @param singleSampleLimit The limit on the largest node that can be + * approximated by sampling. This defaults to 20. */ - void ResetQueryTree(); + RASearch(TreeType* referenceTree, + const bool singleMode = false, + const double tau = 5, + const double alpha = 0.95, + const bool sampleAtLeaves = false, + const bool firstLeafExact = false, + const size_t singleSampleLimit = 20, + const MetricType metric = MetricType()); - // Returns a string representation of this object. + /** + * Delete the RASearch object. The tree is the only member we are + * responsible for deleting. The others will take care of themselves. + */ + ~RASearch(); + + /** + * Compute the rank approximate nearest neighbors of each query point in the + * query set and store the output in the given matrices. The matrices will be + * set to the size of n columns by k rows, where n is the number of points in + * the query dataset and k is the number of neighbors being searched for. + * + * If querySet is small or only contains one point, it can be faster to do + * single-tree search; single-tree search can be set with the SingleMode() + * function or in the constructor. + * + * @param querySet Set of query points (can be a single point). + * @param k Number of neighbors to search for. + * @param neighbors Matrix storing lists of neighbors for each query point. + * @param distances Matrix storing distances of neighbors for each query + * point. + */ + void Search(const typename TreeType::Mat& querySet, + const size_t k, + arma::Mat& neighbors, + arma::mat& distances); + + /** + * Compute the rank approximate nearest neighbors of each point in the + * pre-built query tree and store the output in the given matrices. The + * matrices will be set to the size of n columns by k rows, where n is the + * number of points in the query dataset and k is the number of neighbors + * being searched for. + * + * If singleMode or naive is enabled, then this method will throw a + * std::invalid_argument exception; calling this function implies a dual-tree + * algorithm. + * + * @note + * If the tree type you are using modifies the data matrix, be aware that the + * results returned from this function will be with respect to the modified + * data matrix. + * @endnote + * + * @param queryTree Tree built on query points. + * @param k Number of neighbors to search for. + * @param neighbors Matrix storing lists of neighbors for each query point. + * @param distances Matrix storing distances of neighbors for each query + * point. + */ + void Search(TreeType* queryTree, + const size_t k, + arma::Mat& neighbors, + arma::mat& distances); + + /** + * Compute the rank approximate nearest neighbors of each point in the + * reference set (that is, the query set is taken to be the reference set), + * and store the output in the given matrices. The matrices will be set to + * the size of n columns by k rows, where n is the number of points in the + * query dataset and k is the number of neighbors being searched for. + * + * @param k Number of neighbors to search for. + * @param neighbors Matrix storing lists of neighbors for each point. + * @param distances Matrix storing distances of neighbors for each query + * point. + */ + void Search(const size_t k, + arma::Mat& neighbors, + arma::mat& distances); + + /** + * This function recursively resets the RAQueryStat of the given query tree to + * set 'bound' to SortPolicy::WorstDistance and 'numSamplesMade' to 0. This + * allows a user to perform multiple searches with the same query tree, + * possibly with different levels of approximation without requiring to build + * a new pair of trees for every new (approximate) search. + * + * If Search() is called multiple times with the same query tree without + * calling ResetQueryTree(), the results may not satisfy the theoretical + * guarantees provided by the rank-approximate neighbor search algorithm. + * + * @param queryTree Tree whose statistics should be reset. + */ + void ResetQueryTree(TreeType* queryTree) const; + + //! Get the rank-approximation in percentile of the data. + double Tau() const { return tau; } + //! Modify the rank-approximation in percentile of the data. + double& Tau() { return tau; } + + //! Get the desired success probability. + double Alpha() const { return alpha; } + //! Modify the desired success probability. + double& Alpha() { return alpha; } + + //! Get whether or not sampling is done at the leaves. + bool SampleAtLeaves() const { return sampleAtLeaves; } + //! Modify whether or not sampling is done at the leaves. + bool& SampleAtLeaves() { return sampleAtLeaves; } + + //! Get whether or not we traverse to the first leaf without approximation. + bool FirstLeafExact() const { return firstLeafExact; } + //! Modify whether or not we traverse to the first leaf without approximation. + bool& FirstLeafExact() { return firstLeafExact; } + + //! Get the limit on the size of a node that can be approximated. + size_t SingleSampleLimit() const { return singleSampleLimit; } + //! Modify the limit on the size of a node that can be approximation. + size_t& SingleSampleLimit() { return singleSampleLimit; } + + //! Returns a string representation of this object. std::string ToString() const; private: //! Copy of reference dataset (if we need it, because tree building modifies //! it). arma::mat referenceCopy; - //! Copy of query dataset (if we need it, because tree building modifies it). - arma::mat queryCopy; - //! Reference dataset. const arma::mat& referenceSet; - //! Query dataset (may not be given). - const arma::mat& querySet; - //! Pointer to the root of the reference tree. TreeType* referenceTree; - //! Pointer to the root of the query tree (might not exist). - TreeType* queryTree; //! If true, this object created the trees and is responsible for them. bool treeOwner; - //! Indicates if a separate query set was passed. - bool hasQuerySet; //! Indicates if naive random sampling on the set is being used. bool naive; //! Indicates if single-tree search is being used (opposed to dual-tree). bool singleMode; + //! The rank-approximation in percentile of the data (between 0 and 100). + double tau; + //! The desired success probability (between 0 and 1). + double alpha; + //! Whether or not sampling is done at the leaves. Faster, but less accurate. + bool sampleAtLeaves; + //! If true, we will traverse to the first leaf without approximation. + bool firstLeafExact; + //! The limit on the number of points in the largest node that can be + //! approximated by sampling. + size_t singleSampleLimit; + //! Instantiation of kernel. MetricType metric; //! Permutations of reference points during tree building. std::vector oldFromNewReferences; - //! Permutations of query points during tree building. - std::vector oldFromNewQueries; - //! Total number of pruned nodes during the neighbor search. - size_t numberOfPrunes; - - /** - * @param treeNode The node of the tree whose RAQueryStat is reset - * and whose children are to be explored recursively. - */ - void ResetRAQueryStat(TreeType* treeNode); }; // class RASearch }; // namespace neighbor diff --git a/src/mlpack/methods/rann/ra_search_impl.hpp b/src/mlpack/methods/rann/ra_search_impl.hpp index 50188b32ea..aca74606ce 100644 --- a/src/mlpack/methods/rann/ra_search_impl.hpp +++ b/src/mlpack/methods/rann/ra_search_impl.hpp @@ -41,53 +41,7 @@ TreeType* BuildTree( return new TreeType(dataset); } -}; // namespace aux - -// Construct the object. -template -RASearch:: -RASearch(const typename TreeType::Mat& referenceSetIn, - const typename TreeType::Mat& querySetIn, - const bool naive, - const bool singleMode, - const MetricType metric) : - referenceSet(tree::TreeTraits::RearrangesDataset ? referenceCopy : - referenceSetIn), - querySet((tree::TreeTraits::RearrangesDataset && !singleMode) ? - queryCopy : querySetIn), - referenceTree(NULL), - queryTree(NULL), - treeOwner(!naive), - hasQuerySet(true), - naive(naive), - singleMode(!naive && singleMode), // No single mode if naive. - metric(metric), - numberOfPrunes(0) -{ - // We'll time tree building. - Timer::Start("tree_building"); - - if (tree::TreeTraits::RearrangesDataset) - { - referenceCopy = referenceSetIn; - if (!singleMode) - queryCopy = querySetIn; - } - - // Construct as a naive object if we need to. - if (!naive) - { - referenceTree = aux::BuildTree(const_cast(referenceSet), oldFromNewReferences); - - if (!singleMode) - queryTree = aux::BuildTree(const_cast(querySet), oldFromNewQueries); - } - - // Stop the timer we started above. - Timer::Stop("tree_building"); -} +} // namespace aux // Construct the object. template @@ -95,30 +49,37 @@ RASearch:: RASearch(const typename TreeType::Mat& referenceSetIn, const bool naive, const bool singleMode, + const double tau, + const double alpha, + const bool sampleAtLeaves, + const bool firstLeafExact, + const size_t singleSampleLimit, const MetricType metric) : - referenceSet(tree::TreeTraits::RearrangesDataset ? referenceCopy : - referenceSetIn), - querySet(tree::TreeTraits::RearrangesDataset && !singleMode ? - referenceCopy : referenceSetIn), + referenceSet((tree::TreeTraits::RearrangesDataset && !naive) + ? referenceCopy : referenceSetIn), referenceTree(NULL), - queryTree(NULL), treeOwner(!naive), - hasQuerySet(false), naive(naive), singleMode(!naive && singleMode), // No single mode if naive. - metric(metric), - numberOfPrunes(0) + tau(tau), + alpha(alpha), + sampleAtLeaves(sampleAtLeaves), + firstLeafExact(firstLeafExact), + singleSampleLimit(singleSampleLimit), + metric(metric) { // We'll time tree building. Timer::Start("tree_building"); - if (tree::TreeTraits::RearrangesDataset) - referenceCopy = referenceSetIn; - - // Construct as a naive object if we need to. if (!naive) - referenceTree = aux::BuildTree(const_cast(referenceSet), oldFromNewReferences); + { + if (tree::TreeTraits::RearrangesDataset) + referenceCopy = referenceSetIn; + + referenceTree = aux::BuildTree( + const_cast(referenceSet), + oldFromNewReferences); + } // Stop the timer we started above. Timer::Stop("tree_building"); @@ -128,44 +89,27 @@ RASearch(const typename TreeType::Mat& referenceSetIn, template RASearch:: RASearch(TreeType* referenceTree, - TreeType* queryTree, - const typename TreeType::Mat& referenceSet, - const typename TreeType::Mat& querySet, const bool singleMode, + const double tau, + const double alpha, + const bool sampleAtLeaves, + const bool firstLeafExact, + const size_t singleSampleLimit, const MetricType metric) : - referenceSet(referenceSet), - querySet(querySet), + referenceSet(referenceTree->Dataset()), referenceTree(referenceTree), - queryTree(queryTree), treeOwner(false), - hasQuerySet(true), naive(false), singleMode(singleMode), - metric(metric), - numberOfPrunes(0) + tau(tau), + alpha(alpha), + sampleAtLeaves(sampleAtLeaves), + firstLeafExact(firstLeafExact), + singleSampleLimit(singleSampleLimit), + metric(metric) // Nothing else to initialize. { } -// Construct the object. -template -RASearch:: -RASearch(TreeType* referenceTree, - const typename TreeType::Mat& referenceSet, - const bool singleMode, - const MetricType metric) : - referenceSet(referenceSet), - querySet(referenceSet), - referenceTree(referenceTree), - queryTree(NULL), - treeOwner(false), - hasQuerySet(false), - naive(false), - singleMode(singleMode), - metric(metric), - numberOfPrunes(0) -// Nothing else to initialize. -{ } - /** * The tree is the only member we may be responsible for deleting. The others * will take care of themselves. @@ -174,13 +118,8 @@ template RASearch:: ~RASearch() { - if (treeOwner) - { - if (referenceTree) - delete referenceTree; - if (queryTree) - delete queryTree; - } + if (treeOwner && referenceTree) + delete referenceTree; } /** @@ -189,30 +128,30 @@ RASearch:: */ template void RASearch:: -Search(const size_t k, - arma::Mat& resultingNeighbors, - arma::mat& distances, - const double tau, - const double alpha, - const bool sampleAtLeaves, - const bool firstLeafExact, - const size_t singleSampleLimit) +Search(const typename TreeType::Mat& querySet, + const size_t k, + arma::Mat& neighbors, + arma::mat& distances) { Timer::Start("computing_neighbors"); + // This will hold mappings for query points, if necessary. + std::vector oldFromNewQueries; + // If we have built the trees ourselves, then we will have to map all the // indices back to their original indices when this computation is finished. // To avoid an extra copy, we will store the neighbors and distances in a // separate matrix. - arma::Mat* neighborPtr = &resultingNeighbors; + arma::Mat* neighborPtr = &neighbors; arma::mat* distancePtr = &distances; // Mapping is only required if this tree type rearranges points and we are not // in naive mode. if (tree::TreeTraits::RearrangesDataset) { - if (treeOwner && !(singleMode && hasQuerySet)) + if (!singleMode && !naive) distancePtr = new arma::mat; // Query indices need to be mapped. + if (treeOwner) neighborPtr = new arma::Mat; // All indices need mapping. } @@ -222,18 +161,25 @@ Search(const size_t k, distancePtr->set_size(k, querySet.n_cols); distancePtr->fill(SortPolicy::WorstDistance()); - size_t numPrunes = 0; + // If we will be building a tree and it will modify the query set, make a copy + // of the dataset. + typename TreeType::Mat queryCopy; + const bool needsCopy = (!naive && !singleMode && + tree::TreeTraits::RearrangesDataset); + if (needsCopy) + queryCopy = querySet; + + const typename TreeType::Mat& querySetRef = (needsCopy) ? queryCopy : + querySet; + + // Create the helper object for the tree traversal. + typedef RASearchRules RuleType; + RuleType rules(referenceSet, querySetRef, *neighborPtr, *distancePtr, + metric, tau, alpha, naive, sampleAtLeaves, firstLeafExact, + singleSampleLimit, false); if (naive) { - // We don't need to run the base case on every possible combination of - // points; we can achieve the rank approximation guarantee with probability - // alpha by sampling the reference set. - typedef RASearchRules RuleType; - RuleType rules(referenceSet, querySet, *neighborPtr, *distancePtr, - metric, tau, alpha, naive, sampleAtLeaves, firstLeafExact, - singleSampleLimit); - // Find how many samples from the reference set we need and sample uniformly // from the reference set without replacement. const size_t numSamples = rules.MinimumSamplesReqd(referenceSet.n_cols, k, @@ -244,19 +190,12 @@ Search(const size_t k, // Run the base case on each combination of query point and sampled // reference point. - for (size_t i = 0; i < querySet.n_cols; ++i) + for (size_t i = 0; i < querySetRef.n_cols; ++i) for (size_t j = 0; j < distinctSamples.n_elem; ++j) rules.BaseCase(i, (size_t) distinctSamples[j]); } else if (singleMode) { - // Create the helper object for the tree traversal. Initialization of - // RASearchRules already implicitly performs the naive tree traversal. - typedef RASearchRules RuleType; - RuleType rules(referenceSet, querySet, *neighborPtr, *distancePtr, - metric, tau, alpha, naive, sampleAtLeaves, firstLeafExact, - singleSampleLimit); - // If the reference root node is a leaf, then the sampling has already been // done in the RASearchRules constructor. This happens when naive = true. if (!referenceTree->IsLeaf()) @@ -268,11 +207,9 @@ Search(const size_t k, traverser(rules); // Now have it traverse for each point. - for (size_t i = 0; i < querySet.n_cols; ++i) + for (size_t i = 0; i < querySetRef.n_cols; ++i) traverser.Traverse(i, *referenceTree); - numPrunes = traverser.NumPrunes(); - Log::Info << "Single-tree traversal complete." << std::endl; Log::Info << "Average number of distance calculations per query point: " << (rules.NumDistComputations() / querySet.n_cols) << "." @@ -283,27 +220,20 @@ Search(const size_t k, { Log::Info << "Performing dual-tree traversal..." << std::endl; - typedef RASearchRules RuleType; - RuleType rules(referenceSet, querySet, *neighborPtr, *distancePtr, - metric, tau, alpha, sampleAtLeaves, firstLeafExact, - singleSampleLimit); + // Build the query tree. + Timer::Stop("computing_neighbors"); + Timer::Start("tree_building"); + TreeType* queryTree = aux::BuildTree( + const_cast(querySetRef), oldFromNewQueries); + Timer::Stop("tree_building"); + Timer::Start("computing_neighbors"); typename TreeType::template DualTreeTraverser traverser(rules); - if (queryTree) - { - Log::Info << "Query statistic pre-search: " - << queryTree->Stat().NumSamplesMade() << std::endl; - traverser.Traverse(*queryTree, *referenceTree); - } - else - { - Log::Info << "Query statistic pre-search: " - << referenceTree->Stat().NumSamplesMade() << std::endl; - traverser.Traverse(*referenceTree, *referenceTree); - } + Log::Info << "Query statistic pre-search: " + << queryTree->Stat().NumSamplesMade() << std::endl; - numPrunes = traverser.NumPrunes(); + traverser.Traverse(*queryTree, *referenceTree); Log::Info << "Dual-tree traversal complete." << std::endl; Log::Info << "Average number of distance calculations per query point: " @@ -311,114 +241,257 @@ Search(const size_t k, } Timer::Stop("computing_neighbors"); - Log::Info << "Pruned " << numPrunes << " nodes." << std::endl; - // Now, do we need to do mapping of indices? - if (!treeOwner || !tree::TreeTraits::RearrangesDataset) + // Map points back to original indices, if necessary. + if (tree::TreeTraits::RearrangesDataset) { - // No mapping needed. We are done. - return; - } - else if (treeOwner && hasQuerySet && !singleMode) // Map both sets. - { - // Set size of output matrices correctly. - resultingNeighbors.set_size(k, querySet.n_cols); - distances.set_size(k, querySet.n_cols); - - for (size_t i = 0; i < distances.n_cols; i++) + if (!singleMode && !naive && treeOwner) { - // Map distances (copy a column). - distances.col(oldFromNewQueries[i]) = distancePtr->col(i); + // We must map both query and reference indices. + neighbors.set_size(k, querySet.n_cols); + distances.set_size(k, querySet.n_cols); + + for (size_t i = 0; i < distances.n_cols; i++) + { + // Map distances (copy a column). + distances.col(oldFromNewQueries[i]) = distancePtr->col(i); + + // Map indices of neighbors. + for (size_t j = 0; j < distances.n_rows; j++) + { + neighbors(j, oldFromNewQueries[i]) = + oldFromNewReferences[(*neighborPtr)(j, i)]; + } + } + + // Finished with temporary matrices. + delete neighborPtr; + delete distancePtr; + } + else if (!singleMode && !naive) + { + // We must map query indices only. + neighbors.set_size(k, querySet.n_cols); + distances.set_size(k, querySet.n_cols); + + for (size_t i = 0; i < distances.n_cols; ++i) + { + // Map distances (copy a column). + const size_t queryMapping = oldFromNewQueries[i]; + distances.col(queryMapping) = distancePtr->col(i); + neighbors.col(queryMapping) = neighborPtr->col(i); + } + + // Finished with temporary matrices. + delete neighborPtr; + delete distancePtr; + } + else if (treeOwner) + { + // We must map reference indices only. + neighbors.set_size(k, querySet.n_cols); // Map indices of neighbors. - for (size_t j = 0; j < distances.n_rows; j++) - { - resultingNeighbors(j, oldFromNewQueries[i]) = - oldFromNewReferences[(*neighborPtr)(j, i)]; - } + for (size_t i = 0; i < neighbors.n_cols; i++) + for (size_t j = 0; j < neighbors.n_rows; j++) + neighbors(j, i) = oldFromNewReferences[(*neighborPtr)(j, i)]; + + // Finished with temporary matrix. + delete neighborPtr; + } + } +} + +template +void RASearch::Search( + TreeType* queryTree, + const size_t k, + arma::Mat& neighbors, + arma::mat& distances) +{ + Timer::Start("computing_neighbors"); + + // Get a reference to the query set. + const typename TreeType::Mat& querySet = queryTree->Dataset(); + + // Make sure we are in dual-tree mode. + if (singleMode || naive) + throw std::invalid_argument("cannot call NeighborSearch::Search() with a " + "query tree when naive or singleMode are set to true"); + + // We won't need to map query indices, but will we need to map distances? + arma::Mat* neighborPtr = &neighbors; + + if (treeOwner && tree::TreeTraits::RearrangesDataset) + neighborPtr = new arma::Mat; + + neighborPtr->set_size(k, querySet.n_cols); + neighborPtr->fill(size_t() - 1); + distances.set_size(k, querySet.n_cols); + distances.fill(SortPolicy::WorstDistance()); + + // Create the helper object for the tree traversal. + typedef RASearchRules RuleType; + RuleType rules(referenceSet, queryTree->Dataset(), *neighborPtr, distances, + metric, tau, alpha, naive, sampleAtLeaves, firstLeafExact, + singleSampleLimit, false); + + // Create the traverser. + typename TreeType::template DualTreeTraverser traverser(rules); + traverser.Traverse(*queryTree, *referenceTree); + + Timer::Stop("computing_neighbors"); + + // Do we need to map indices? + if (treeOwner && tree::TreeTraits::RearrangesDataset) + { + // We must map reference indices only. + neighbors.set_size(k, querySet.n_cols); + + // Map indices of neighbors. + for (size_t i = 0; i < neighbors.n_cols; i++) + for (size_t j = 0; j < neighbors.n_rows; j++) + neighbors(j, i) = oldFromNewReferences[(*neighborPtr)(j, i)]; + + // Finished with temporary matrix. + delete neighborPtr; + } +} + +template +void RASearch::Search( + const size_t k, + arma::Mat& neighbors, + arma::mat& distances) +{ + Timer::Start("computing_neighbors"); + + arma::Mat* neighborPtr = &neighbors; + arma::mat* distancePtr = &distances; + + if (tree::TreeTraits::RearrangesDataset && treeOwner) + { + // We will always need to rearrange in this case. + distancePtr = new arma::mat; + neighborPtr = new arma::Mat; + } + + // Initialize results. + neighborPtr->set_size(k, referenceSet.n_cols); + neighborPtr->fill(size_t() - 1); + distancePtr->set_size(k, referenceSet.n_cols); + distancePtr->fill(SortPolicy::WorstDistance()); + + // Create the helper object for the tree traversal. + typedef RASearchRules RuleType; + RuleType rules(referenceSet, referenceSet, *neighborPtr, *distancePtr, + metric, tau, alpha, naive, sampleAtLeaves, firstLeafExact, + singleSampleLimit, true /* sets are the same */); + + if (naive) + { + // Find how many samples from the reference set we need and sample uniformly + // from the reference set without replacement. + const size_t numSamples = rules.MinimumSamplesReqd(referenceSet.n_cols, k, + tau, alpha); + arma::uvec distinctSamples; + rules.ObtainDistinctSamples(numSamples, referenceSet.n_cols, + distinctSamples); + + // The naive brute-force solution. + for (size_t i = 0; i < referenceSet.n_cols; ++i) + for (size_t j = 0; j < referenceSet.n_cols; ++j) + rules.BaseCase(i, j); + } + else if (singleMode) + { + // Create the traverser. + typename TreeType::template SingleTreeTraverser traverser(rules); + + // Now have it traverse for each point. + for (size_t i = 0; i < referenceSet.n_cols; ++i) + traverser.Traverse(i, *referenceTree); + } + else + { + // Create the traverser. + typename TreeType::template DualTreeTraverser traverser(rules); + + traverser.Traverse(*referenceTree, *referenceTree); + } + + Timer::Stop("computing_neighbors"); + + // Do we need to map the reference indices? + if (treeOwner && tree::TreeTraits::RearrangesDataset) + { + neighbors.set_size(k, referenceSet.n_cols); + distances.set_size(k, referenceSet.n_cols); + + for (size_t i = 0; i < distances.n_cols; ++i) + { + // Map distances (copy a column). + const size_t refMapping = oldFromNewReferences[i]; + distances.col(refMapping) = distancePtr->col(i); + + // Map each neighbor's index. + for (size_t j = 0; j < distances.n_rows; ++j) + neighbors(j, refMapping) = oldFromNewReferences[(*neighborPtr)(j, i)]; } // Finished with temporary matrices. delete neighborPtr; delete distancePtr; } - else if (treeOwner && !hasQuerySet) - { - // No query tree -- map both references and queries. - resultingNeighbors.set_size(k, querySet.n_cols); - distances.set_size(k, querySet.n_cols); - - for (size_t i = 0; i < distances.n_cols; i++) - { - // Map distances (copy a column). - distances.col(oldFromNewReferences[i]) = distancePtr->col(i); - - // Map indices of neighbors. - for (size_t j = 0; j < distances.n_rows; j++) - { - resultingNeighbors(j, oldFromNewReferences[i]) = - oldFromNewReferences[(*neighborPtr)(j, i)]; - } - } - } - else if (treeOwner && hasQuerySet && singleMode) // Map only references. - { - // Set size of neighbor indices matrix correctly. - resultingNeighbors.set_size(k, querySet.n_cols); - - // Map indices of neighbors. - for (size_t i = 0; i < resultingNeighbors.n_cols; i++) - { - for (size_t j = 0; j < resultingNeighbors.n_rows; j++) - { - resultingNeighbors(j, i) = oldFromNewReferences[(*neighborPtr)(j, i)]; - } - } - - // Finished with temporary matrix. - delete neighborPtr; - } -} // Search - -template -void RASearch::ResetQueryTree() -{ - if (!singleMode) - { - if (queryTree) - ResetRAQueryStat(queryTree); - else - ResetRAQueryStat(referenceTree); - } } template -void RASearch::ResetRAQueryStat( - TreeType* treeNode) +void RASearch::ResetQueryTree( + TreeType* queryNode) const { - treeNode->Stat().Bound() = SortPolicy::WorstDistance(); - treeNode->Stat().NumSamplesMade() = 0; + queryNode->Stat().Bound() = SortPolicy::WorstDistance(); + queryNode->Stat().NumSamplesMade() = 0; - for (size_t i = 0; i < treeNode->NumChildren(); i++) - ResetRAQueryStat(&treeNode->Child(i)); + for (size_t i = 0; i < queryNode->NumChildren(); i++) + ResetQueryTree(&queryNode->Child(i)); } -// Returns a String of the Object. +// Returns a string representation of the object. template std::string RASearch::ToString() const { std::ostringstream convert; - convert << "RA Search [" << this << "]" << std::endl; - convert << " Reference Set: " << referenceSet.n_rows << "x" ; - convert << referenceSet.n_cols << std::endl; - if (&referenceSet != &querySet) - convert << " QuerySet: " << querySet.n_rows << "x" << querySet.n_cols - << std::endl; + convert << "RASearch [" << this << "]" << std::endl; + convert << " referenceSet: " << referenceSet.n_rows << "x" + << referenceSet.n_cols << std::endl; + + convert << " naive: "; if (naive) - convert << " Naive: TRUE" << std::endl; + convert << "true" << std::endl; + else + convert << "false" << std::endl; + + convert << " singleMode: "; if (singleMode) - convert << " Single Node: TRUE" << std::endl; - convert << " Metric: " << std::endl << + convert << "true" << std::endl; + else + convert << "false" << std::endl; + + convert << " tau: " << tau << std::endl; + convert << " alpha: " << alpha << std::endl; + convert << " sampleAtLeaves: "; + if (sampleAtLeaves) + convert << "true" << std::endl; + else + convert << "false" << std::endl; + + convert << " firstLeafExact: "; + if (firstLeafExact) + convert << "true" << std::endl; + else + convert << "false" << std::endl; + convert << " singleSampleLimit: " << singleSampleLimit << std::endl; + convert << " metric: " << std::endl << mlpack::util::Indent(metric.ToString(),2); return convert.str(); } diff --git a/src/mlpack/methods/rann/ra_search_rules.hpp b/src/mlpack/methods/rann/ra_search_rules.hpp index 73d6b27eb7..2642afe2e1 100644 --- a/src/mlpack/methods/rann/ra_search_rules.hpp +++ b/src/mlpack/methods/rann/ra_search_rules.hpp @@ -29,9 +29,8 @@ class RASearchRules const bool naive = false, const bool sampleAtLeaves = false, const bool firstLeafExact = false, - const size_t singleSampleLimit = 20); - - + const size_t singleSampleLimit = 20, + const bool sameSet = false); double BaseCase(const size_t queryIndex, const size_t referenceIndex); @@ -229,6 +228,9 @@ class RASearchRules // TO REMOVE: just for testing size_t numDistComputations; + //! If the query and reference set are identical, this is true. + bool sameSet; + TraversalInfoType traversalInfo; /** diff --git a/src/mlpack/methods/rann/ra_search_rules_impl.hpp b/src/mlpack/methods/rann/ra_search_rules_impl.hpp index 2fbfe585af..77a5b0ef4f 100644 --- a/src/mlpack/methods/rann/ra_search_rules_impl.hpp +++ b/src/mlpack/methods/rann/ra_search_rules_impl.hpp @@ -25,15 +25,17 @@ RASearchRules(const arma::mat& referenceSet, const bool naive, const bool sampleAtLeaves, const bool firstLeafExact, - const size_t singleSampleLimit) : - referenceSet(referenceSet), - querySet(querySet), - neighbors(neighbors), - distances(distances), - metric(metric), - sampleAtLeaves(sampleAtLeaves), - firstLeafExact(firstLeafExact), - singleSampleLimit(singleSampleLimit) + const size_t singleSampleLimit, + const bool sameSet) : + referenceSet(referenceSet), + querySet(querySet), + neighbors(neighbors), + distances(distances), + metric(metric), + sampleAtLeaves(sampleAtLeaves), + firstLeafExact(firstLeafExact), + singleSampleLimit(singleSampleLimit), + sameSet(sameSet) { // Validate tau to make sure that the rank approximation is greater than the // number of neighbors requested. @@ -268,7 +270,7 @@ double RASearchRules::BaseCase( { // If the datasets are the same, then this search is only using one dataset // and we should not return identical points. - if ((&querySet == &referenceSet) && (queryIndex == referenceIndex)) + if (sameSet && (queryIndex == referenceIndex)) return 0.0; double distance = metric.Evaluate(querySet.unsafe_col(queryIndex),