Making sparse works in incremental mapping

This commit is contained in:
matlabbe
2026-08-19 14:52:50 -07:00
parent 99aa79319b
commit 6a45a26161
4 changed files with 372 additions and 197 deletions
+49 -12
View File
@@ -31,9 +31,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "rtabmap/core/rtabmap_core_export.h" // DLL export/import defines #include "rtabmap/core/rtabmap_core_export.h" // DLL export/import defines
#include <opencv2/core/core.hpp> #include <opencv2/core/core.hpp>
#include <Eigen/SparseCore>
#include <list> #include <list>
#include <map>
#include <set> #include <set>
#include <utility>
#include <vector> #include <vector>
#include "rtabmap/utilite/UEventsHandler.h" #include "rtabmap/utilite/UEventsHandler.h"
#include "rtabmap/core/Parameters.h" #include "rtabmap/core/Parameters.h"
@@ -154,7 +155,9 @@ public:
* *
* @param memory Working memory instance (must not be null). * @param memory Working memory instance (must not be null).
* @param ids Ordered list of signature ids (often includes @ref Memory::kIdVirtual as first element). * @param ids Ordered list of signature ids (often includes @ref Memory::kIdVirtual as first element).
* @return Square CV_32FC1 matrix of size ids.size() × ids.size(). * @return Square CV_32FC1 matrix of size ids.size() × ids.size(), or an empty matrix when
* @ref Parameters::kBayesSparsePrediction() is enabled and the prediction is being
* kept in its sparse form, in which case no matrix exists to return.
*/ */
cv::Mat generatePrediction(const Memory * memory, const std::vector<int> & ids); cv::Mat generatePrediction(const Memory * memory, const std::vector<int> & ids);
@@ -221,20 +224,42 @@ private:
bool generateSparsePrediction(const Memory * memory, const std::vector<int> & ids); bool generateSparsePrediction(const Memory * memory, const std::vector<int> & ids);
/** /**
* @brief Rebuilds the sparse prediction from the prediction matrix. * @brief Carries the sparse prediction over to a longer list of ids, without rebuilding it.
* *
* Called only when the matrix has changed, and only when it is being kept anyway, which * Every id already there keeps its index when ids are only appended, so the columns
* is the case while mapping. Leaves the sparse form empty, which makes * already built still apply and only the ones the appended ids reach are built again.
* @ref computePosterior() fall back to the dense multiplication, when the matrix is not *
* sparse (see @ref generateSparsePrediction()). * @param memory Working memory instance (must not be null).
* @param oldIds The ids the prediction was built for.
* @param newIds The ids it should be indexed by.
* @return False when @p newIds is not @p oldIds with more appended, which the caller has
* to answer by building the prediction again with @ref generateSparsePrediction():
* an id removed shifts the index of every one after it. Also false when
* @ref Parameters::kBayesFullPredictionUpdate() asks for a full rebuild.
*/ */
void updateSparsePredictionFromDense(); bool updateSparsePrediction(const Memory * memory,
const std::vector<int> & oldIds,
const std::vector<int> & newIds);
/** /**
* @brief Appends the non-zero values of a built column, and zeroes the buffer. * @brief Takes the non-zero values of a built column into the prediction, and zeroes the buffer.
*
* @param column Buffer holding the column, zeroed on return.
* @param index Index of the column in the prediction.
* @param withRoomToGrow Gives the column slightly more room than its values need, so that
* rebuilding it into a few more values does not have to move it.
*/ */
void appendSparseColumn(std::vector<float> & column, int index, void takeSparsePredictionColumn(std::vector<float> & column, int index, bool withRoomToGrow);
std::vector<Eigen::Triplet<float> > & triplets) const;
/**
* @brief Packs the columns into the order they are multiplied in, each with the room it needs.
*/
void compactSparsePrediction();
/**
* @brief The neighborhood of an id from @ref _neighborsIndex, querying and caching it if absent.
*/
const std::map<int, int> & cachedNeighbors(const Memory * memory, int id);
/** /**
* @brief Releases the sparse prediction and the memory it holds. * @brief Releases the sparse prediction and the memory it holds.
@@ -268,7 +293,19 @@ private:
bool _sparsePrediction; ///< Multiply the prediction sparsely (Bayes/SparsePrediction). bool _sparsePrediction; ///< Multiply the prediction sparsely (Bayes/SparsePrediction).
bool _predictionChanged; ///< True when _prediction was rebuilt, so the sparse form is stale. bool _predictionChanged; ///< True when _prediction was rebuilt, so the sparse form is stale.
bool _sparsePredictionRejected; ///< True when the current prediction was measured as too dense to keep sparse. bool _sparsePredictionRejected; ///< True when the current prediction was measured as too dense to keep sparse.
Eigen::SparseMatrix<float, Eigen::RowMajor> _sparsePredictionMatrix; ///< The prediction, sparse. Built instead of _prediction over a fixed graph. /// Where a column of the sparse prediction sits in _sparsePredictionValues, and how much
/// room it was given: a column rebuilt into more values than it has room for is moved to
/// the end, leaving its room behind until compactSparsePrediction() recovers it.
struct SparseColumn
{
size_t offset = 0;
size_t size = 0;
size_t capacity = 0;
};
std::vector<SparseColumn> _sparsePredictionColumns; ///< The columns of the sparse prediction, built instead of _prediction.
std::vector<std::pair<int, float> > _sparsePredictionValues; ///< The (row, value) of every non-zero, column by column.
size_t _sparsePredictionUsed = 0; ///< How many of _sparsePredictionValues belong to a column.
std::vector<int> _sparsePredictionIds; ///< The ids _sparsePredictionColumns is indexed by.
std::map<int, std::map<int, int> > _neighborsIndex; ///< Cached neighbor margins per signature id. std::map<int, std::map<int, int> > _neighborsIndex; ///< Cached neighbor margins per signature id.
}; };
+268 -168
View File
@@ -230,26 +230,35 @@ const std::map<int, float> & BayesFilter::computePosterior(const Memory * memory
// Recursive Bayes estimation... // Recursive Bayes estimation...
// STEP 1 - Prediction : Prior*lastPosterior // STEP 1 - Prediction : Prior*lastPosterior
// //
// The prediction is built in its sparse form only, the matrix never being // The prediction is kept in its sparse form only, the matrix never being allocated:
// allocated, when the graph is fixed: in localization mode there is no incremental // built once, then carried over to the locations of the next iteration. Over a fixed
// update of the matrix to carry columns over, so nothing else needs it. While // graph nothing changes and there is nothing to do; while mapping, the appended
// mapping, the matrix is built as before and the sparse form is taken from it. // locations reach only a few of the columns and only those are built again. A location
// leaving the working memory shifts the index of every one after it, and is answered by
// building the prediction again, which is what the dense update does then as well.
if(!sameIds) if(!sameIds)
{ {
// The locations changed, so the prediction has to be built again, and whether it // The locations changed, so whether the prediction is worth keeping sparse is a
// is worth keeping sparse is a question about the new one. // question about the new one.
_predictionChanged = true; _predictionChanged = true;
_sparsePredictionRejected = false; _sparsePredictionRejected = false;
} }
const bool buildSparseDirectly = const bool keepSparse =
_sparsePrediction && _sparsePrediction &&
!_sparsePredictionRejected && !_sparsePredictionRejected &&
_totalPredictionLCValues >= 1 && _totalPredictionLCValues >= 1;
!memory->isIncremental();
bool sparseBuilt = false; bool sparseBuilt = false;
if(buildSparseDirectly) if(keepSparse)
{ {
if(_predictionChanged) if(!_predictionChanged && _sparsePredictionIds == ids)
{
sparseBuilt = true;
}
else if(this->updateSparsePrediction(memory, _sparsePredictionIds, ids))
{
sparseBuilt = true;
}
else
{ {
sparseBuilt = this->generateSparsePrediction(memory, ids); sparseBuilt = this->generateSparsePrediction(memory, ids);
// Measured as too dense to be worth it: the matrix is built instead, and not // Measured as too dense to be worth it: the matrix is built instead, and not
@@ -257,43 +266,17 @@ const std::map<int, float> & BayesFilter::computePosterior(const Memory * memory
// iteration would cost more than the multiplication it is trying to save. // iteration would cost more than the multiplication it is trying to save.
_sparsePredictionRejected = !sparseBuilt; _sparsePredictionRejected = !sparseBuilt;
} }
else _predictionChanged = false;
{ UDEBUG("STEP1-generate prior=%fs, columns=%d", timer.ticks(),
sparseBuilt = _sparsePredictionMatrix.rows() > 0; (int)_sparsePredictionColumns.size());
}
UDEBUG("STEP1-generate prior=%fs, rows=%d, cols=%d", timer.ticks(),
(int)_sparsePredictionMatrix.rows(), (int)_sparsePredictionMatrix.cols());
} }
if(!sparseBuilt) if(!sparseBuilt)
{ {
this->clearSparsePrediction();
_prediction = this->generatePrediction(memory, ids); _prediction = this->generatePrediction(memory, ids);
_predictionChanged = false;
UDEBUG("STEP1-generate prior=%fs, rows=%d, cols=%d", timer.ticks(), _prediction.rows, _prediction.cols); UDEBUG("STEP1-generate prior=%fs, rows=%d, cols=%d", timer.ticks(), _prediction.rows, _prediction.cols);
//std::cout << "Prediction=" << _prediction << std::endl; //std::cout << "Prediction=" << _prediction << std::endl;
// Taking the sparse form from the matrix reads all of the matrix, which is the
// work of one dense multiplication: it pays for itself on the second
// multiplication of a prediction, not on the first. So it is left until the
// prediction is seen to outlast an iteration. A mapping session changes it on
// every location it adds and never pays for a form it would not multiply twice,
// while a session sitting on the same graph pays once and gains on every
// iteration after.
if(_sparsePrediction && !_sparsePredictionRejected && !_prediction.empty())
{
if(_predictionChanged)
{
// A prediction of its own: whatever was built for the previous one is stale.
this->clearSparsePrediction();
}
else if(_sparsePredictionMatrix.rows() != _prediction.rows)
{
// The prediction of the last iteration, so it is being multiplied more than
// once and its sparse form is worth the read.
this->updateSparsePredictionFromDense();
_sparsePredictionRejected = _sparsePredictionMatrix.rows() == 0;
UDEBUG("STEP1-sparse prediction update time=%fs", timer.ticks());
}
}
_predictionChanged = false;
} }
// Adjust the last posterior if some images were // Adjust the last posterior if some images were
@@ -312,7 +295,7 @@ const std::map<int, float> & BayesFilter::computePosterior(const Memory * memory
// (m,m) X (m,1) = (m,1) // (m,m) X (m,1) = (m,1)
// The sparse form is empty when disabled, or when the prediction was found too // The sparse form is empty when disabled, or when the prediction was found too
// dense for it to be worth it. // dense for it to be worth it.
const bool sparse = _sparsePrediction && _sparsePredictionMatrix.rows() > 0; const bool sparse = _sparsePrediction && !_sparsePredictionColumns.empty();
if(sparse) if(sparse)
{ {
this->multiplySparsePrediction(posterior, prior); this->multiplySparsePrediction(posterior, prior);
@@ -441,16 +424,18 @@ std::map<int, int> resolveNeighbors(
cv::Mat BayesFilter::generatePrediction(const Memory * memory, const std::vector<int> & ids) cv::Mat BayesFilter::generatePrediction(const Memory * memory, const std::vector<int> & ids)
{ {
if(this->posteriorHasSameIds(ids)) if(!_sparsePredictionColumns.empty())
{
// The prediction is being kept sparse, so there is no matrix: building one to hand
// over would cost the memory and the time that not building it is saving.
return cv::Mat();
}
if(!_prediction.empty() && this->posteriorHasSameIds(ids))
{ {
return _prediction; return _prediction;
} }
std::vector<int> oldIds = uKeys(_posterior); std::vector<int> oldIds = uKeys(_posterior);
// Both paths below return a newly built matrix, so the sparse view of the
// previous one no longer applies.
_predictionChanged = true;
if(!_fullPredictionUpdate && !_prediction.empty()) if(!_fullPredictionUpdate && !_prediction.empty())
{ {
return updatePrediction(_prediction, memory, oldIds, ids); return updatePrediction(_prediction, memory, oldIds, ids);
@@ -528,47 +513,114 @@ cv::Mat BayesFilter::generatePrediction(const Memory * memory, const std::vector
return prediction; return prediction;
} }
// Appends the non zero values of a freshly built column to the triplets of the sparse // Takes the non zero values of a freshly built column into the prediction, and leaves the
// prediction, and leaves the buffer zeroed for the next one, which saves clearing the // buffer zeroed for the next one, which saves clearing the whole of it every time.
// whole of it every time. // Takes the non zero values of a freshly built column into the prediction, and leaves the
void BayesFilter::appendSparseColumn( // buffer zeroed for the next one, which saves clearing the whole of it every time.
std::vector<float> & column, //
int index, // The values of every column live in one array, so that the multiplication reads them the
std::vector<Eigen::Triplet<float> > & triplets) const // way memory likes to be read. A column keeps the room it was given: rebuilt into fewer
// values it stays where it is, rebuilt into more than it has room for it is put at the end
// and the room it had is left behind, to be recovered by compactSparsePrediction(). Asking
// for a little more than is needed, when the column is one being rebuilt, buys the room for
// it to grow a few times in place.
void BayesFilter::takeSparsePredictionColumn(std::vector<float> & column, int index, bool withRoomToGrow)
{ {
size_t count = 0;
for(size_t row=0; row<column.size(); ++row) for(size_t row=0; row<column.size(); ++row)
{ {
if(column[row] != 0.0f) if(column[row] != 0.0f)
{ {
triplets.push_back(Eigen::Triplet<float>((int)row, index, column[row])); ++count;
}
}
SparseColumn & slot = _sparsePredictionColumns[index];
_sparsePredictionUsed -= slot.size;
if(count > slot.capacity)
{
slot.offset = _sparsePredictionValues.size();
slot.capacity = withRoomToGrow ? count + count/8 + 4 : count;
_sparsePredictionValues.resize(slot.offset + slot.capacity);
}
slot.size = count;
_sparsePredictionUsed += count;
size_t i = slot.offset;
for(size_t row=0; row<column.size(); ++row)
{
if(column[row] != 0.0f)
{
_sparsePredictionValues[i++] = std::make_pair((int)row, column[row]);
column[row] = 0.0f; column[row] = 0.0f;
} }
} }
} }
// The prediction built directly in its sparse form, the matrix never being allocated. // Packs the columns back into the order they are multiplied in, giving each the room it
// needs and no more. Called when the room left behind by rebuilt columns has grown to a
// quarter of what is in use, and at the end of a full build, whose columns are not built in
// the order of their index.
void BayesFilter::compactSparsePrediction()
{
std::vector<std::pair<int, float> > packed;
packed.reserve(_sparsePredictionUsed);
for(size_t i=0; i<_sparsePredictionColumns.size(); ++i)
{
SparseColumn & slot = _sparsePredictionColumns[i];
const size_t offset = packed.size();
packed.insert(packed.end(),
_sparsePredictionValues.begin()+slot.offset,
_sparsePredictionValues.begin()+slot.offset+slot.size);
slot.offset = offset;
slot.capacity = slot.size;
}
_sparsePredictionValues.swap(packed);
}
// The neighborhood of a location, from the cache the incremental update needs, adding it
// there and to the neighborhoods of its own neighbors when it is not there yet.
const std::map<int, int> & BayesFilter::cachedNeighbors(const Memory * memory, int id)
{
std::map<int, std::map<int, int> >::const_iterator iter = _neighborsIndex.find(id);
if(iter == _neighborsIndex.end())
{
std::map<int, int> neighbors = memory->getNeighborsId(id, _predictionLC.size()-1, 0, false, false, true, true);
for(std::map<int, int>::iterator jter=neighbors.begin(); jter!=neighbors.end(); ++jter)
{
std::map<int, std::map<int, int> >::iterator kter = _neighborsIndex.find(jter->first);
if(kter != _neighborsIndex.end())
{
uInsert(kter->second, std::make_pair(id, jter->second));
}
}
iter = _neighborsIndex.insert(std::make_pair(id, neighbors)).first;
}
return iter->second;
}
// The prediction built in its sparse form, the matrix never being allocated.
// //
// A column of the prediction only holds the neighbors of one location within the depth // A column of the prediction only holds the neighbors of one location within the depth of
// of the prediction model, so on a large map the matrix is mostly zeros, while holding // the prediction model, so on a large map the matrix is mostly zeros, while holding it
// it costs the size of the working memory squared against the far smaller size of the // costs the size of the working memory squared against the far smaller size of the values
// values in it. Each column is built in a buffer of its own instead, through the same // in it. Each column is built in a buffer of its own instead, through the same
// addNeighborProb() and normalize() as the dense build, and only its non zero values // addNeighborProb() and normalize() as the dense build, and only its non zero values are
// are kept. // kept. The columns are kept apart rather than in one array so that
// updateSparsePrediction() can replace one of them.
// //
// The columns are not built in the order of their index: a column is built for every // The columns are not built in the order of their index: a column is built for every
// location at margin 0 of the one being expanded, so several are built at once. Eigen // location at margin 0 of the one being expanded, so several are built at once.
// takes them as triplets and orders them.
// //
// Returns false when the matrix would not be sparse, which the caller has to answer by // Returns false when the prediction would not be sparse, which the caller has to answer by
// building the dense one. Only reachable through a model whose values sum to less than // building the dense matrix. Happens when the values of the model sum to less than 1, as
// 1, as normalize() then spreads the missing probability over every zero of a column. // normalize() then spreads the missing probability over every zero of a column.
bool BayesFilter::generateSparsePrediction(const Memory * memory, const std::vector<int> & ids) bool BayesFilter::generateSparsePrediction(const Memory * memory, const std::vector<int> & ids)
{ {
UASSERT(memory && _predictionLC.size() >= 2 && ids.size()); UASSERT(memory && _predictionLC.size() >= 2 && ids.size());
UTimer timer; UTimer timer;
this->clearSparsePrediction(); this->clearSparsePrediction();
_predictionChanged = false;
const int size = (int)ids.size(); const int size = (int)ids.size();
@@ -584,15 +636,15 @@ bool BayesFilter::generateSparsePrediction(const Memory * memory, const std::vec
} }
} }
// A value costs 12 bytes as a triplet and 8 in the matrix, against the 4 of the // A value costs 8 bytes kept sparse against the 4 of the matrix, so past a quarter
// dense one, so past a quarter filled the sparse form is not worth building. // filled the sparse form is not worth building.
const size_t maxValues = (size_t)size*(size_t)size/4; const size_t maxValues = (size_t)size*(size_t)size/4;
// The neighborhood of a few locations, to know whether the prediction is worth // The neighborhood of a few locations, to know whether the prediction is worth keeping
// keeping sparse before building all of it. A loop closure link costs no margin, so // sparse before building all of it. A loop closure link costs no margin, so on a
// on a densely linked graph a column reaches most of the map and there is nothing // densely linked graph a column reaches most of the map and there is nothing sparse to
// sparse to keep; finding that out by building a quarter of it first would cost more // keep; finding that out by building a quarter of it first would cost more than the
// than the multiplications it is trying to save. // multiplications it is trying to save.
{ {
const int samples = size < 64 ? size : 64; const int samples = size < 64 ? size : 64;
size_t reached = 0; size_t reached = 0;
@@ -636,8 +688,14 @@ bool BayesFilter::generateSparsePrediction(const Memory * memory, const std::vec
} }
} }
// The neighborhood a column was built from is kept only when locations can be added,
// which is what updateSparsePrediction() needs it for. Over a fixed graph nothing is
// ever appended, and one neighborhood per location is as much memory again as the
// values of the prediction.
std::map<int, std::map<int, int> > * cache = memory->isIncremental() ? &_neighborsIndex : 0;
_sparsePredictionColumns.assign(size, SparseColumn());
std::vector<float> column(size, 0.0f); std::vector<float> column(size, 0.0f);
std::vector<Eigen::Triplet<float> > triplets;
std::set<int> idsDone; std::set<int> idsDone;
for(int i=0; i<size; ++i) for(int i=0; i<size; ++i)
@@ -650,147 +708,189 @@ bool BayesFilter::generateSparsePrediction(const Memory * memory, const std::vec
if(ids[i] > 0) if(ids[i] > 0)
{ {
std::list<int> idsLoopMargin; std::list<int> idsLoopMargin;
// The neighborhoods are not kept in _neighborsIndex: it is there for the
// incremental updatePrediction(), which this build replaces, and it holds
// one neighborhood per location, as much memory again as the values here.
std::map<int, int> neighbors = resolveNeighbors( std::map<int, int> neighbors = resolveNeighbors(
memory, ids[i], _predictionLC.size()-1, idToIndexMap, idsLoopMargin, 0); memory, ids[i], _predictionLC.size()-1, idToIndexMap, idsLoopMargin, cache);
// same neighbor tree for loop signatures (margin = 0) // same neighbor tree for loop signatures (margin = 0)
for(std::list<int>::iterator iter=idsLoopMargin.begin(); iter!=idsLoopMargin.end(); ++iter) for(std::list<int>::iterator iter=idsLoopMargin.begin(); iter!=idsLoopMargin.end(); ++iter)
{ {
if(cache)
{
uInsert(*cache, std::make_pair(*iter, neighbors));
}
const int index = idToIndexMap.at(*iter); const int index = idToIndexMap.at(*iter);
float sum = addNeighborProb(&column[0], 1, neighbors, _predictionLC, idToIndexMap); const float sum = addNeighborProb(&column[0], 1, neighbors, _predictionLC, idToIndexMap);
idsDone.insert(*iter);
this->normalize(&column[0], 1, size, index, sum, ids[0]<0); this->normalize(&column[0], 1, size, index, sum, ids[0]<0);
this->appendSparseColumn(column, index, triplets); this->takeSparsePredictionColumn(column, index, false);
idsDone.insert(*iter);
} }
} }
else else
{ {
this->fillVirtualPlaceColumn(&column[0], 1, size); this->fillVirtualPlaceColumn(&column[0], 1, size);
this->appendSparseColumn(column, i, triplets); this->takeSparsePredictionColumn(column, i, false);
}
if(triplets.size() > maxValues)
{
UWARN("The prediction has more than %ld non zero values, which is too dense "
"for %s to be worth it (%d of %d locations done). Building the matrix "
"instead. The values of %s summing to less than 1 is what fills it.",
(long)maxValues, Parameters::kBayesSparsePrediction().c_str(), i+1, size,
Parameters::kBayesPredictionLC().c_str());
return false;
} }
} }
// The columns were not built in the order of their index, so they are packed into it.
this->compactSparsePrediction();
_sparsePredictionIds = ids;
_sparsePredictionMatrix.resize(size, size); const size_t nnz = _sparsePredictionUsed;
_sparsePredictionMatrix.setFromTriplets(triplets.begin(), triplets.end());
UDEBUG("Sparse prediction: %ld/%ld values (%.2f%%), %ld MB against the %ld MB of the " UDEBUG("Sparse prediction: %ld/%ld values (%.2f%%), %ld MB against the %ld MB of the "
"matrix, built in %fs", "matrix, built in %fs",
(long)_sparsePredictionMatrix.nonZeros(), (long)size*size, (long)nnz, (long)size*size, 100.0*double(nnz)/(double(size)*double(size)),
100.0*double(_sparsePredictionMatrix.nonZeros())/(double(size)*double(size)),
(long)(this->getSparsePredictionMemoryUsed()/1048576), (long)(this->getSparsePredictionMemoryUsed()/1048576),
(long)((size_t)size*(size_t)size*sizeof(float)/1048576), (long)((size_t)size*(size_t)size*sizeof(float)/1048576),
timer.ticks()); timer.ticks());
return true; return true;
} }
// The same sparse prediction, but taken from the matrix rather than built instead of // The same prediction after locations were appended, without building it again.
// it. Used when the matrix is there anyway, which is the case while mapping: the //
// incremental updatePrediction() needs it to carry the unchanged columns over. // Every location that was already there keeps its index, so the columns already built
void BayesFilter::updateSparsePredictionFromDense() // still apply: only the ones the new locations reach have to be built again, and the
// column of the virtual place, whose values are shared out over however many locations
// there are. What a column holds does not otherwise depend on how many there are, the
// model summing to 1 leaving normalize() nothing to spread over the others.
//
// Returns false when the locations are not the previous ones with more appended, which the
// caller has to answer by building the prediction again: a location removed shifts the
// index of every one after it, and the dense update has to recompute every column then too.
bool BayesFilter::updateSparsePrediction(
const Memory * memory,
const std::vector<int> & oldIds,
const std::vector<int> & newIds)
{ {
this->clearSparsePrediction(); if(_fullPredictionUpdate ||
_predictionChanged = false; oldIds.empty() ||
newIds.size() <= oldIds.size() ||
if(_prediction.empty()) _sparsePredictionColumns.size() != oldIds.size() ||
memcmp(oldIds.data(), newIds.data(), oldIds.size()*sizeof(int)) != 0)
{ {
return; return false;
}
UASSERT(_prediction.type() == CV_32FC1);
UASSERT(_prediction.isContinuous());
// normalize() spreads the probability that the model doesn't account for over every
// zero of a column, so with such a model there is no zero left to skip. The
// condition is the one used there, so that both agree on the matrix content.
if(_totalPredictionLCValues < 1)
{
UWARN("%s is enabled but the values of %s sum to %f < 1, so the missing "
"probability is spread over all the other locations and the prediction "
"matrix has no zeros to skip. Using the dense multiplication instead. "
"Make the values sum to 1 to benefit from the sparse one.",
Parameters::kBayesSparsePrediction().c_str(),
Parameters::kBayesPredictionLC().c_str(),
_totalPredictionLCValues);
return;
} }
UTimer timer; UTimer timer;
const int rows = _prediction.rows; const int size = (int)newIds.size();
const int cols = _prediction.cols;
const float * data = (const float *)_prediction.data;
const size_t maxValues = (size_t)rows*(size_t)cols/4;
// Read in the order the matrix is stored in, a column of it being strided. IdToIndexMap newIdToIndexMap;
std::vector<Eigen::Triplet<float> > triplets; #if __cplusplus >= 201103L
for(int row=0; row<rows; ++row) newIdToIndexMap.reserve(newIds.size());
#endif
for(int i=0; i<size; ++i)
{ {
const float * rowPtr = data + (size_t)row*cols; if(newIds[i]>0)
for(int col=0; col<cols; ++col)
{ {
if(rowPtr[col] != 0.0f) newIdToIndexMap[newIds[i]] = i;
{
triplets.push_back(Eigen::Triplet<float>(row, col, rowPtr[col]));
}
}
if(triplets.size() > maxValues)
{
UWARN("The prediction matrix has more than %ld non zero values, which is too "
"dense for %s to be worth it (%d of %d rows scanned). Using the dense "
"multiplication instead.",
(long)maxValues, Parameters::kBayesSparsePrediction().c_str(), row+1, rows);
return;
} }
} }
_sparsePredictionMatrix.resize(rows, cols); _sparsePredictionColumns.resize(size); // the appended columns start out empty
_sparsePredictionMatrix.setFromTriplets(triplets.begin(), triplets.end()); std::vector<float> column(size, 0.0f);
UDEBUG("Sparse prediction: %ld/%ld values (%.2f%%), %ld MB on top of the %ld MB of " // The appended locations, and the ones that were already there whose neighborhood the
"the matrix, built in %fs", // appended ones are now part of.
(long)_sparsePredictionMatrix.nonZeros(), (long)rows*cols, std::set<int> idsToUpdate;
100.0*double(_sparsePredictionMatrix.nonZeros())/(double(rows)*double(cols)), for(size_t i=oldIds.size(); i<newIds.size(); ++i)
(long)(this->getSparsePredictionMemoryUsed()/1048576), {
(long)(_prediction.total()*_prediction.elemSize()/1048576), if(newIds[i] <= 0)
{
continue;
}
const std::map<int, int> & neighbors = this->cachedNeighbors(memory, newIds[i]);
const float sum = addNeighborProb(&column[0], 1, neighbors, _predictionLC, newIdToIndexMap);
this->normalize(&column[0], 1, size, (int)i, sum, newIds[0]<0);
this->takeSparsePredictionColumn(column, (int)i, true);
for(std::map<int, int>::const_iterator iter=neighbors.begin(); iter!=neighbors.end(); ++iter)
{
const IdToIndexMap::const_iterator jter = newIdToIndexMap.find(iter->first);
if(jter != newIdToIndexMap.end() && (size_t)jter->second < oldIds.size())
{
idsToUpdate.insert(iter->first);
}
}
}
for(std::set<int>::const_iterator iter=idsToUpdate.begin(); iter!=idsToUpdate.end(); ++iter)
{
const std::map<int, std::map<int, int> >::const_iterator kter = _neighborsIndex.find(*iter);
UASSERT_MSG(kter != _neighborsIndex.end(),
uFormat("Did not find %d (current index size=%d)", *iter, (int)_neighborsIndex.size()).c_str());
const int index = newIdToIndexMap.at(*iter);
const float sum = addNeighborProb(&column[0], 1, kter->second, _predictionLC, newIdToIndexMap);
this->normalize(&column[0], 1, size, index, sum, newIds[0]<0);
this->takeSparsePredictionColumn(column, index, true);
}
// The virtual place shares what is left of its probability over the visited locations,
// so its column depends on how many of them there are.
if(newIds[0] < 0)
{
this->fillVirtualPlaceColumn(&column[0], 1, size);
this->takeSparsePredictionColumn(column, 0, true);
}
// The room left behind by the columns that outgrew their slot, once it is a quarter of
// what is in use.
const size_t waste = _sparsePredictionValues.size() - _sparsePredictionUsed;
const bool compacted = waste > _sparsePredictionUsed/4;
if(compacted)
{
this->compactSparsePrediction();
}
_sparsePredictionIds = newIds;
UDEBUG("Sparse prediction: %d locations appended, %d columns rebuilt of %d, %ld values, "
"%ld left behind%s, updated in %fs",
(int)(newIds.size()-oldIds.size()), (int)idsToUpdate.size(), size,
(long)_sparsePredictionUsed, (long)waste, compacted?" (packed again)":"",
timer.ticks()); timer.ticks());
return true;
} }
void BayesFilter::clearSparsePrediction() void BayesFilter::clearSparsePrediction()
{ {
// Assigned rather than resized: resize(0,0) keeps the buffers it has allocated. _sparsePredictionColumns.clear();
_sparsePredictionMatrix = Eigen::SparseMatrix<float, Eigen::RowMajor>(); _sparsePredictionValues.clear();
_sparsePredictionIds.clear();
_sparsePredictionUsed = 0;
} }
unsigned long BayesFilter::getSparsePredictionMemoryUsed() const unsigned long BayesFilter::getSparsePredictionMemoryUsed() const
{ {
typedef Eigen::SparseMatrix<float, Eigen::RowMajor>::StorageIndex StorageIndex; return _sparsePredictionValues.capacity() * sizeof(std::pair<int, float>)
return _sparsePredictionMatrix.nonZeros() * (sizeof(float)+sizeof(StorageIndex)) + _sparsePredictionColumns.capacity() * sizeof(SparseColumn)
+ (_sparsePredictionMatrix.outerSize()+1) * sizeof(StorageIndex); + _sparsePredictionIds.capacity() * sizeof(int);
} }
void BayesFilter::multiplySparsePrediction(const cv::Mat & posterior, cv::Mat & prior) const void BayesFilter::multiplySparsePrediction(const cv::Mat & posterior, cv::Mat & prior) const
{ {
UASSERT(_sparsePredictionMatrix.rows() > 0); const int size = (int)_sparsePredictionColumns.size();
UASSERT(size > 0);
UASSERT(posterior.cols == 1 && posterior.type() == CV_32FC1); UASSERT(posterior.cols == 1 && posterior.type() == CV_32FC1);
UASSERT_MSG(posterior.rows == (int)_sparsePredictionMatrix.cols(), UASSERT_MSG(posterior.rows == size,
uFormat("posterior=%d prediction=%d", posterior.rows, uFormat("posterior=%d prediction=%d", posterior.rows, size).c_str());
(int)_sparsePredictionMatrix.cols()).c_str());
prior = cv::Mat((int)_sparsePredictionMatrix.rows(), 1, CV_32FC1); prior = cv::Mat::zeros(size, 1, CV_32FC1);
Eigen::Map<const Eigen::VectorXf> posteriorVector((const float *)posterior.data, posterior.rows); const float * posteriorPtr = (const float *)posterior.data;
Eigen::Map<Eigen::VectorXf> priorVector((float *)prior.data, prior.rows); float * priorPtr = (float *)prior.data;
priorVector.noalias() = _sparsePredictionMatrix * posteriorVector;
// The prior is the sum of the columns of the prediction weighted by the posterior.
// Going by column is the order the values are stored in, and lets a location the
// posterior has ruled out be skipped whole.
for(int col=0; col<size; ++col)
{
const float weight = posteriorPtr[col];
if(weight == 0.0f)
{
continue;
}
const SparseColumn & slot = _sparsePredictionColumns[col];
for(size_t i=slot.offset; i<slot.offset+slot.size; ++i)
{
priorPtr[_sparsePredictionValues[i].first] += _sparsePredictionValues[i].second * weight;
}
}
} }
unsigned long BayesFilter::getMemoryUsed() const unsigned long BayesFilter::getMemoryUsed() const
+9
View File
@@ -5809,6 +5809,15 @@ void Rtabmap::dumpPrediction() const
} }
} }
cv::Mat prediction = _bayesFilter->generatePrediction(_memory, uListToVector(signaturesToCompare)); cv::Mat prediction = _bayesFilter->generatePrediction(_memory, uListToVector(signaturesToCompare));
if(prediction.empty())
{
UERROR("The prediction matrix cannot be exported while %s is enabled: the "
"prediction is kept in a sparse form and the matrix is never built. Set "
"%s=false to export it.",
Parameters::kBayesSparsePrediction().c_str(),
Parameters::kBayesSparsePrediction().c_str());
return;
}
FILE* fout = 0; FILE* fout = 0;
std::string fileName = this->getWorkingDir() + "/DumpPrediction.txt"; std::string fileName = this->getWorkingDir() + "/DumpPrediction.txt";
+46 -17
View File
@@ -431,10 +431,14 @@ TEST(BayesFilterTest, GeneratePredictionVirtualPlaceOnly)
EXPECT_FLOAT_EQ(prediction.at<float>(0, 0), 1.0f); EXPECT_FLOAT_EQ(prediction.at<float>(0, 0), 1.0f);
} }
// The prediction matrix is kept between iterations and returned as it is while the ids do
// not change. Only the dense mode has a matrix at all: with the prediction kept sparse,
// generatePrediction() has to build one to return, so there is nothing to keep.
TEST(BayesFilterTest, GeneratePredictionCachedWhenIdsUnchanged) TEST(BayesFilterTest, GeneratePredictionCachedWhenIdsUnchanged)
{ {
ParametersMap params; ParametersMap params;
params.insert(ParametersPair(Parameters::kBayesPredictionLC(), kPredictionNewPlace10Stay90)); params.insert(ParametersPair(Parameters::kBayesPredictionLC(), kPredictionNewPlace10Stay90));
params.insert(ParametersPair(Parameters::kBayesSparsePrediction(), "false"));
BayesFilter filter(params); BayesFilter filter(params);
Memory memory; Memory memory;
@@ -1078,11 +1082,9 @@ TEST_F(BayesFilterMemoryFixture, SparsePredictionFallsBackWhenModelSumsBelowOne)
EXPECT_EQ(filterDense.getMemoryUsed(), filterSparse.getMemoryUsed()); EXPECT_EQ(filterDense.getMemoryUsed(), filterSparse.getMemoryUsed());
} }
// While the prediction matrix is being kept (mapping mode), its sparse form is only taken // The sparse prediction holds only the non-zero values, so it is a fraction of what the
// from it once the prediction outlasts an iteration: reading the whole matrix to build the // matrix would be. Against the dense filter, which holds the matrix and no sparse form,
// form costs the work of one multiplication, so on a prediction that is multiplied once it // this one holds the sparse form and no matrix, and comes out smaller.
// would never be repaid. The sparse form then holds only the non-zero values, so it is a
// fraction of the matrix, and the reported memory reflects that it is an addition to it.
TEST_F(BayesFilterMemoryFixture, SparsePredictionMemoryUsed) TEST_F(BayesFilterMemoryFixture, SparsePredictionMemoryUsed)
{ {
addChain(40); addChain(40);
@@ -1100,20 +1102,16 @@ TEST_F(BayesFilterMemoryFixture, SparsePredictionMemoryUsed)
filterDense.computePosterior(memory_, likelihood); filterDense.computePosterior(memory_, likelihood);
filterSparse.computePosterior(memory_, likelihood); filterSparse.computePosterior(memory_, likelihood);
// The first iteration built the matrix, and nothing else: as far as this iteration
// knows, the prediction is about to be replaced by the next one.
EXPECT_EQ(filterDense.getMemoryUsed(), filterSparse.getMemoryUsed());
// The second finds the same prediction, so its sparse form is worth building.
filterDense.computePosterior(memory_, likelihood);
filterSparse.computePosterior(memory_, likelihood);
const unsigned long dense = filterDense.getMemoryUsed(); const unsigned long dense = filterDense.getMemoryUsed();
const unsigned long sparse = filterSparse.getMemoryUsed(); const unsigned long sparse = filterSparse.getMemoryUsed();
EXPECT_GT(sparse, dense); // Both hold the neighborhood of every location, which a mapping session needs to carry
// The matrix is ids x ids floats; a column of it only holds the neighbors within // the prediction over to the locations it adds, so what separates them is the matrix
// the depth of the model (3 here), so the view has to stay well under it. // against the sparse form of it. The margin is thin at 40 locations: a column is a
EXPECT_LT(sparse - dense, ids.size()*ids.size()*sizeof(float)); // vector of its own so that it can be replaced one at a time, and the few values in a
// column of a map this small barely outweigh the vector holding them.
// SparsePredictionInLocalizationModeDoesNotAllocateTheMatrix has the same comparison
// where the map is large enough for the matrix to dominate.
EXPECT_LT(sparse, dense);
} }
// Where the sparse and dense multiplications have to agree exactly rather than within // Where the sparse and dense multiplications have to agree exactly rather than within
@@ -1278,3 +1276,34 @@ TEST_F(BayesFilterMemoryFixture, SparsePredictionInLocalizationModeDoesNotAlloca
// incremental update of the matrix needs. // incremental update of the matrix needs.
EXPECT_LT(filterSparse.getMemoryUsed(), filterDense.getMemoryUsed()/4); EXPECT_LT(filterSparse.getMemoryUsed(), filterDense.getMemoryUsed()/4);
} }
// generatePrediction() hands over the prediction matrix, which only the dense mode has.
// With the prediction kept sparse there is none, and it says so with an empty matrix rather
// than building one: Rtabmap::dumpPrediction(), the one caller outside the filter, reports
// that the matrix cannot be exported instead of spending the memory and the time that
// keeping the prediction sparse is saving.
TEST_F(BayesFilterMemoryFixture, GeneratePredictionIsEmptyWhileThePredictionIsKeptSparse)
{
addChain(20);
ParametersMap paramsDense;
paramsDense.insert(ParametersPair(Parameters::kBayesPredictionLC(), kPredictionNewPlace10Stay50Neighbor25_15));
paramsDense.insert(ParametersPair(Parameters::kBayesSparsePrediction(), "false"));
ParametersMap paramsSparse = paramsDense;
paramsSparse[Parameters::kBayesSparsePrediction()] = "true";
BayesFilter filterDense(paramsDense);
BayesFilter filterSparse(paramsSparse);
const std::vector<int> ids = getBayesIds();
const std::map<int, float> likelihood = uniformLikelihood(ids);
filterDense.computePosterior(memory_, likelihood);
filterSparse.computePosterior(memory_, likelihood);
const cv::Mat predictionDense = filterDense.generatePrediction(memory_, ids);
ASSERT_EQ(predictionDense.rows, (int)ids.size());
ASSERT_EQ(predictionDense.cols, (int)ids.size());
EXPECT_TRUE(filterSparse.generatePrediction(memory_, ids).empty());
}