mirror of
https://github.com/introlab/rtabmap.git
synced 2026-10-12 04:49:50 +08:00
Sparse Bayes
This commit is contained in:
@@ -119,6 +119,22 @@ IF(BUILD_PERF_TESTS)
|
||||
set_tests_properties(test_flann_index_perf PROPERTIES
|
||||
TIMEOUT ${_perf_timeout}
|
||||
LABELS "performance")
|
||||
|
||||
# Comparison of the dense and the sparse prediction x posterior multiplication of
|
||||
# BayesFilter (Bayes/SparsePrediction), against the size of the map, how connected
|
||||
# its graph is and the depth of the prediction model:
|
||||
# bin/test_bayesfilter_perf
|
||||
# bin/test_bayesfilter_perf --gtest_filter=-*LargeMap*
|
||||
# Its own executable: it spends its time benchmarking rather than asserting, and the
|
||||
# largest map it builds allocates a gigabyte for the dense prediction matrix,
|
||||
# which in a unit test shard would look like a leak.
|
||||
add_executable(test_bayesfilter_perf perf_bayesfilter.cpp)
|
||||
target_link_libraries(test_bayesfilter_perf gtest_main rtabmap_core)
|
||||
|
||||
add_test(NAME test_bayesfilter_perf COMMAND test_bayesfilter_perf)
|
||||
set_tests_properties(test_bayesfilter_perf PROPERTIES
|
||||
TIMEOUT ${_perf_timeout}
|
||||
LABELS "performance")
|
||||
ENDIF(BUILD_PERF_TESTS)
|
||||
|
||||
# Rtabmap end-to-end replay of sample DBs (test data fetched by
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
// Comparison of the dense and the sparse (Parameters::kBayesSparsePrediction())
|
||||
// multiplication of the prediction matrix with the last posterior, which is where
|
||||
// BayesFilter::computePosterior() spends nearly all of its time on a large map.
|
||||
//
|
||||
// Its own executable, run by ctest under the "performance" label, so that its
|
||||
// seconds of benchmarking stay out of the unit test shards:
|
||||
// ctest -L performance to run them
|
||||
// ctest -LE performance to skip them
|
||||
// bin/test_bayesfilter_perf --gtest_filter=*Growing*
|
||||
//
|
||||
// The times are reported rather than asserted on, as they depend on the machine.
|
||||
// What is asserted is that both multiplications give the same posterior, so that
|
||||
// the numbers below compare two ways of computing the same thing.
|
||||
#include <gtest/gtest.h>
|
||||
#include <rtabmap/core/BayesFilter.h>
|
||||
#include <rtabmap/core/Link.h>
|
||||
#include <rtabmap/core/Memory.h>
|
||||
#include <rtabmap/core/Parameters.h>
|
||||
#include <rtabmap/core/SensorData.h>
|
||||
#include <rtabmap/core/Signature.h>
|
||||
#include <rtabmap/core/Transform.h>
|
||||
#include <rtabmap/utilite/UTimer.h>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
using namespace rtabmap;
|
||||
|
||||
namespace {
|
||||
|
||||
// Sizes of the maps compared. The dense prediction matrix is n x n floats, so the
|
||||
// largest one below already allocates 244 MB.
|
||||
static const int MAP_SIZES[] = {1000, 4000, 8000};
|
||||
|
||||
// How many values of the prediction model decides how deep in the graph a column of
|
||||
// the prediction matrix reaches, and thus how many values it holds. The default model
|
||||
// has 18, so it stores neighbors up to 17 links away, with probabilities down to
|
||||
// 6.9e-23. Truncating it to 8 keeps every value above 1e-4 and stops there. The
|
||||
// difference is given to the loop closure probability so that the values still sum to
|
||||
// slightly more than 1: below 1, normalize() spreads what is missing over every zero
|
||||
// of a column and the matrix is no longer sparse at all.
|
||||
static const char PREDICTION_DEFAULT[] =
|
||||
"0.1 0.36 0.30 0.16 0.062 0.0151 0.00255 0.000324 2.5e-05 1.3e-06 4.8e-08 "
|
||||
"1.2e-09 1.9e-11 2.2e-13 1.7e-15 8.5e-18 2.9e-20 6.9e-23";
|
||||
static const char PREDICTION_TRUNCATED[] =
|
||||
"0.1 0.36003 0.30 0.16 0.062 0.0151 0.00255 0.000324";
|
||||
|
||||
// A chain of signatures linked by odometry, with a global loop closure every
|
||||
// loopEvery nodes back to the node loopSpan earlier. The loop closures matter here:
|
||||
// Memory::getNeighborsId() follows them, so each one is a shortcut that widens the
|
||||
// neighborhood a column of the prediction matrix holds. They are what decides how
|
||||
// sparse the matrix is, so they are a knob of these benchmarks rather than a detail.
|
||||
class SyntheticMap
|
||||
{
|
||||
public:
|
||||
// Built by a mapping session, then turned to localization mode unless asked
|
||||
// otherwise: the graph is then fixed, which is what the sparse prediction is built
|
||||
// for, and what the dense one is compared against here.
|
||||
SyntheticMap(int nodes, int loopEvery, int loopSpan, bool localization = true)
|
||||
{
|
||||
ParametersMap params;
|
||||
// No features extracted: the graph is what the prediction matrix is built from.
|
||||
params.insert(ParametersPair(Parameters::kKpMaxFeatures(), "-1"));
|
||||
// Only the latest signature stays in STM, the rest are WM nodes the filter uses.
|
||||
params.insert(ParametersPair(Parameters::kMemSTMSize(), "1"));
|
||||
params.insert(ParametersPair(Parameters::kMemRehearsalSimilarity(), "1.0"));
|
||||
params.insert(ParametersPair(Parameters::kMemBinDataKept(), "false"));
|
||||
memory_ = new Memory(params);
|
||||
|
||||
const cv::Mat image(8, 8, CV_8UC1, cv::Scalar(128));
|
||||
const cv::Mat covariance = cv::Mat::eye(6, 6, CV_64FC1) * 0.01;
|
||||
const cv::Mat information = cv::Mat::eye(6, 6, CV_64FC1);
|
||||
|
||||
UTimer timer;
|
||||
std::vector<int> ids;
|
||||
ids.reserve(nodes);
|
||||
for(int i=0; i<nodes; ++i)
|
||||
{
|
||||
SensorData data(image);
|
||||
UASSERT(memory_->update(data, Transform(float(i), 0.0f, 0.0f, 0, 0, 0), covariance));
|
||||
ids.push_back(memory_->getLastSignatureId());
|
||||
if(loopEvery > 0 && i >= loopSpan && i % loopEvery == 0)
|
||||
{
|
||||
UASSERT(memory_->addLink(Link(
|
||||
ids.back(),
|
||||
ids[ids.size()-1-loopSpan],
|
||||
Link::kGlobalClosure,
|
||||
Transform::getIdentity(),
|
||||
information)));
|
||||
++loopClosures_;
|
||||
}
|
||||
}
|
||||
buildTime_ = timer.ticks();
|
||||
|
||||
if(localization)
|
||||
{
|
||||
ParametersMap localizationParams;
|
||||
localizationParams.insert(ParametersPair(Parameters::kMemIncrementalMemory(), "false"));
|
||||
memory_->parseParameters(localizationParams);
|
||||
UASSERT(!memory_->isIncremental());
|
||||
}
|
||||
}
|
||||
|
||||
~SyntheticMap()
|
||||
{
|
||||
delete memory_;
|
||||
}
|
||||
|
||||
const Memory * memory() const {return memory_;}
|
||||
int loopClosures() const {return loopClosures_;}
|
||||
double buildTime() const {return buildTime_;}
|
||||
|
||||
// What Rtabmap passes to the filter: the virtual place (new location hypothesis)
|
||||
// followed by the WM locations that are not in STM.
|
||||
std::vector<int> bayesIds() const
|
||||
{
|
||||
std::vector<int> ids;
|
||||
ids.push_back(Memory::kIdVirtual);
|
||||
const std::set<int> & stm = memory_->getStMem();
|
||||
for(std::map<int, double>::const_iterator iter=memory_->getWorkingMem().begin();
|
||||
iter!=memory_->getWorkingMem().end();
|
||||
++iter)
|
||||
{
|
||||
if(iter->first > 0 && stm.find(iter->first) == stm.end())
|
||||
{
|
||||
ids.push_back(iter->first);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
// Uniform, which is the worst case for the sparse multiplication: no location is
|
||||
// ruled out, so no column of the prediction can be skipped whole.
|
||||
std::map<int, float> uniformLikelihood(const std::vector<int> & ids) const
|
||||
{
|
||||
std::map<int, float> likelihood;
|
||||
for(size_t i=0; i<ids.size(); ++i)
|
||||
{
|
||||
likelihood.insert(std::make_pair(ids[i], 1.0f));
|
||||
}
|
||||
return likelihood;
|
||||
}
|
||||
|
||||
private:
|
||||
Memory * memory_ = nullptr;
|
||||
int loopClosures_ = 0;
|
||||
double buildTime_ = 0.0;
|
||||
};
|
||||
|
||||
struct Result
|
||||
{
|
||||
double firstIteration = 0.0; // includes generating the prediction matrix, and its sparse view
|
||||
double steadyState = 0.0; // mean of the following iterations, the matrix being unchanged
|
||||
unsigned long memoryUsed = 0;
|
||||
std::map<int, float> posterior;
|
||||
};
|
||||
|
||||
Result run(const SyntheticMap & map, const char * predictionLC, bool sparse, int iterations)
|
||||
{
|
||||
ParametersMap params;
|
||||
params.insert(ParametersPair(Parameters::kBayesPredictionLC(), predictionLC));
|
||||
params.insert(ParametersPair(Parameters::kBayesSparsePrediction(), sparse?"true":"false"));
|
||||
BayesFilter filter(params);
|
||||
|
||||
const std::vector<int> ids = map.bayesIds();
|
||||
const std::map<int, float> likelihood = map.uniformLikelihood(ids);
|
||||
|
||||
Result result;
|
||||
UTimer timer;
|
||||
filter.computePosterior(map.memory(), likelihood);
|
||||
result.firstIteration = timer.ticks();
|
||||
|
||||
double total = 0.0;
|
||||
for(int i=1; i<iterations; ++i)
|
||||
{
|
||||
timer.restart();
|
||||
filter.computePosterior(map.memory(), likelihood);
|
||||
total += timer.ticks();
|
||||
}
|
||||
result.steadyState = iterations > 1 ? total/double(iterations-1) : result.firstIteration;
|
||||
result.memoryUsed = filter.getMemoryUsed();
|
||||
result.posterior = filter.getPosterior();
|
||||
return result;
|
||||
}
|
||||
|
||||
// The two multiplications sum the products of a row in a different order, so the
|
||||
// posteriors differ by the rounding of a few thousand float additions rather than
|
||||
// being bit identical. Reported relative to the largest probability, which is the
|
||||
// scale Rtabmap compares hypotheses at.
|
||||
double maxPosteriorDifference(const std::map<int, float> & a, const std::map<int, float> & b)
|
||||
{
|
||||
if(a.size() != b.size())
|
||||
{
|
||||
return 1.0;
|
||||
}
|
||||
double maxDiff = 0.0;
|
||||
double maxValue = 0.0;
|
||||
for(std::map<int, float>::const_iterator iter=a.begin(); iter!=a.end(); ++iter)
|
||||
{
|
||||
std::map<int, float>::const_iterator jter = b.find(iter->first);
|
||||
if(jter == b.end())
|
||||
{
|
||||
return 1.0;
|
||||
}
|
||||
maxDiff = std::max(maxDiff, std::fabs(double(iter->second) - double(jter->second)));
|
||||
maxValue = std::max(maxValue, std::fabs(double(iter->second)));
|
||||
}
|
||||
return maxValue > 0.0 ? maxDiff/maxValue : maxDiff;
|
||||
}
|
||||
|
||||
void report(const char * name, const Result & result)
|
||||
{
|
||||
printf("[ ] %-7s first iteration %9.1f ms, steady state %8.2f ms, filter memory %7.1f MB\n",
|
||||
name, result.firstIteration*1000.0, result.steadyState*1000.0, result.memoryUsed/1048576.0);
|
||||
}
|
||||
|
||||
void compare(const SyntheticMap & map, const char * predictionLC, int iterations)
|
||||
{
|
||||
const size_t size = map.bayesIds().size();
|
||||
const Result dense = run(map, predictionLC, false, iterations);
|
||||
const Result sparse = run(map, predictionLC, true, iterations);
|
||||
|
||||
report("dense", dense);
|
||||
report("sparse", sparse);
|
||||
printf("[ ] steady state speedup x%.1f, dense/sparse memory x%.2f, "
|
||||
"relative posterior difference %.1e\n",
|
||||
sparse.steadyState > 0.0 ? dense.steadyState/sparse.steadyState : 0.0,
|
||||
sparse.memoryUsed > 0 ? double(dense.memoryUsed)/double(sparse.memoryUsed) : 0.0,
|
||||
maxPosteriorDifference(dense.posterior, sparse.posterior));
|
||||
|
||||
EXPECT_EQ(dense.posterior.size(), size);
|
||||
// Same probabilities up to the rounding of the sums, which the iterations compound:
|
||||
// the sums are of a few thousand floats spanning the whole range of the model, down
|
||||
// to 6.9e-23 for the default one, and each iteration starts from the previous
|
||||
// posterior. Which location comes out highest is not compared: the likelihood is
|
||||
// uniform here, so the visited locations are all within rounding of each other and
|
||||
// the highest is whichever the rounding favors.
|
||||
EXPECT_LT(maxPosteriorDifference(dense.posterior, sparse.posterior), 1e-3);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Both modes on the same graph, over maps of growing size. The dense multiplication
|
||||
// reads the whole n x n matrix on every iteration, so its cost grows with the square
|
||||
// of the number of nodes, while the sparse one grows with the number of values the
|
||||
// graph actually puts in the matrix.
|
||||
TEST(BayesFilterPerfTest, DenseVsSparsePredictionOnGrowingMaps)
|
||||
{
|
||||
// The steady state of the smaller maps is a fraction of a millisecond, so it is
|
||||
// averaged over enough iterations that one hiccup doesn't carry the mean.
|
||||
const int iterations = 30;
|
||||
|
||||
for(size_t s=0; s<sizeof(MAP_SIZES)/sizeof(int); ++s)
|
||||
{
|
||||
const int nodes = MAP_SIZES[s];
|
||||
// A moderately connected graph, so that this measures the effect of the size.
|
||||
// How much the connectivity itself matters is measured by the test below.
|
||||
SyntheticMap map(nodes, 100, 500);
|
||||
const size_t size = map.bayesIds().size();
|
||||
|
||||
std::cout << "[ ] " << nodes << " nodes (" << size << " ids with the virtual place), "
|
||||
<< map.loopClosures() << " loop closures, graph built in " << map.buildTime()
|
||||
<< "s, dense matrix = " << (size*size*sizeof(float))/1048576 << " MB" << std::endl;
|
||||
|
||||
compare(map, PREDICTION_DEFAULT, iterations);
|
||||
}
|
||||
}
|
||||
|
||||
// How sparse the prediction matrix is, and so how much the sparse multiplication can
|
||||
// win, is decided by two things: how connected the graph is, every loop closure being
|
||||
// a shortcut that getNeighborsId() follows, and how deep the prediction model reaches.
|
||||
// The default 18 values model stores neighbors up to 17 links away with probabilities
|
||||
// down to 6.9e-23, which are numerically irrelevant next to the 0.36 of the first
|
||||
// level but fill most of the matrix.
|
||||
TEST(BayesFilterPerfTest, SparsityAgainstGraphConnectivityAndModelDepth)
|
||||
{
|
||||
const int nodes = 4000;
|
||||
const int iterations = 30;
|
||||
|
||||
struct Connectivity { const char * name; int loopEvery; int loopSpan; };
|
||||
const Connectivity connectivities[] = {
|
||||
{"chain only, no loop closure", 0, 0},
|
||||
{"a loop closure every 100 nodes, spanning 500", 100, 500},
|
||||
{"a loop closure every 20 nodes, spanning 200", 20, 200},
|
||||
};
|
||||
|
||||
for(size_t c=0; c<sizeof(connectivities)/sizeof(Connectivity); ++c)
|
||||
{
|
||||
SyntheticMap map(nodes, connectivities[c].loopEvery, connectivities[c].loopSpan);
|
||||
std::cout << "[ ] " << nodes << " nodes, " << connectivities[c].name
|
||||
<< " (" << map.loopClosures() << " loop closures)" << std::endl;
|
||||
|
||||
std::cout << "[ ] 18 values model (default, depth 17):" << std::endl;
|
||||
compare(map, PREDICTION_DEFAULT, iterations);
|
||||
std::cout << "[ ] 8 values model (depth 7, every value above 1e-4):" << std::endl;
|
||||
compare(map, PREDICTION_TRUNCATED, iterations);
|
||||
}
|
||||
}
|
||||
|
||||
// The size of a real large map, over which a localization session iterates without
|
||||
// ever changing the graph: the prediction matrix is generated once and every
|
||||
// following iteration reuses it, so the sparse view is built once too. This is the
|
||||
// case the sparse multiplication is for.
|
||||
//
|
||||
// The dense matrix alone is a gigabyte at that size, and takes seconds to generate;
|
||||
// exclude this one with
|
||||
// bin/test_bayesfilter_perf --gtest_filter=-*LargeMap*
|
||||
TEST(BayesFilterPerfTest, DenseVsSparsePredictionOnALargeMap)
|
||||
{
|
||||
const int nodes = 16384;
|
||||
const int iterations = 10;
|
||||
|
||||
SyntheticMap map(nodes, 100, 500);
|
||||
const size_t size = map.bayesIds().size();
|
||||
std::cout << "[ ] " << nodes << " nodes (" << size << " ids with the virtual place), "
|
||||
<< map.loopClosures() << " loop closures, graph built in " << map.buildTime()
|
||||
<< "s, dense matrix = " << (size*size*sizeof(float))/1048576 << " MB" << std::endl;
|
||||
|
||||
std::cout << "[ ] 18 values model (default, depth 17):" << std::endl;
|
||||
compare(map, PREDICTION_DEFAULT, iterations);
|
||||
std::cout << "[ ] 8 values model (depth 7, every value above 1e-4):" << std::endl;
|
||||
compare(map, PREDICTION_TRUNCATED, iterations);
|
||||
}
|
||||
|
||||
// While mapping, the graph changes on every node, so the matrix is kept for
|
||||
// updatePrediction() to carry its unchanged columns over and the sparse prediction is
|
||||
// taken from it rather than built instead of it. Same multiplication, no memory saved.
|
||||
TEST(BayesFilterPerfTest, DenseVsSparsePredictionWhileMapping)
|
||||
{
|
||||
const int nodes = 4000;
|
||||
const int iterations = 30;
|
||||
|
||||
SyntheticMap map(nodes, 100, 500, false /*stay in mapping mode*/);
|
||||
const size_t size = map.bayesIds().size();
|
||||
std::cout << "[ ] " << nodes << " nodes, mapping mode (the matrix is kept), "
|
||||
<< map.loopClosures() << " loop closures, dense matrix = "
|
||||
<< (size*size*sizeof(float))/1048576 << " MB" << std::endl;
|
||||
|
||||
compare(map, PREDICTION_DEFAULT, iterations);
|
||||
}
|
||||
@@ -183,6 +183,17 @@ protected:
|
||||
memory_ = 0;
|
||||
}
|
||||
|
||||
// Turns the memory to localization mode, the graph built so far becoming fixed.
|
||||
// Without a database driver the working memory is kept as it is, so this is a
|
||||
// mapping session followed by a localization session over what it mapped.
|
||||
void switchToLocalization()
|
||||
{
|
||||
ParametersMap params;
|
||||
params.insert(ParametersPair(Parameters::kMemIncrementalMemory(), "false"));
|
||||
memory_->parseParameters(params);
|
||||
ASSERT_FALSE(memory_->isIncremental());
|
||||
}
|
||||
|
||||
void addChain(int count)
|
||||
{
|
||||
ASSERT_GT(count, 0);
|
||||
@@ -990,3 +1001,269 @@ TEST_F(BayesFilterMemoryFixture, FullPredictionUpdateRegeneratesMatrix)
|
||||
expectMatrixGrowsOnNewNode(true);
|
||||
expectMatrixGrowsOnNewNode(false);
|
||||
}
|
||||
|
||||
// Bayes/SparsePrediction only changes how the prediction matrix is multiplied with
|
||||
// the last posterior, so both modes must agree, including while the graph grows and
|
||||
// the matrix is rebuilt (the sparse view has to be rebuilt with it).
|
||||
TEST_F(BayesFilterMemoryFixture, CompareSparsePredictionModes)
|
||||
{
|
||||
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);
|
||||
|
||||
addChain(4);
|
||||
|
||||
for(int iter = 0; iter < 8; ++iter)
|
||||
{
|
||||
SensorData data(image_);
|
||||
Transform pose(float(4 + iter), 0.0f, 0.0f, 0, 0, 0);
|
||||
ASSERT_TRUE(memory_->update(data, pose, covariance_));
|
||||
|
||||
const std::vector<int> ids = getBayesIds();
|
||||
std::map<int, float> likelihood = uniformLikelihood(ids);
|
||||
// Favor one location so that the posterior isn't uniform, and zeroes some
|
||||
// others out, which is the case where the sparse multiplication skips columns.
|
||||
likelihood[ids[ids.size()/2]] = 4.0f;
|
||||
|
||||
filterDense.computePosterior(memory_, likelihood);
|
||||
filterSparse.computePosterior(memory_, likelihood);
|
||||
|
||||
const std::map<int, float> & posteriorDense = filterDense.getPosterior();
|
||||
const std::map<int, float> & posteriorSparse = filterSparse.getPosterior();
|
||||
ASSERT_EQ(posteriorDense.size(), posteriorSparse.size());
|
||||
for(size_t i = 0; i < ids.size(); ++i)
|
||||
{
|
||||
EXPECT_NEAR(posteriorDense.at(ids[i]), posteriorSparse.at(ids[i]), 1e-5f)
|
||||
<< "iter=" << iter << " id=" << ids[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A model whose values sum to less than 1 has normalize() spread the difference over
|
||||
// every zero of a column, leaving the matrix dense. The sparse mode then falls back
|
||||
// to the dense multiplication, which must not change the posterior.
|
||||
TEST_F(BayesFilterMemoryFixture, SparsePredictionFallsBackWhenModelSumsBelowOne)
|
||||
{
|
||||
addChain(5);
|
||||
|
||||
ParametersMap paramsDense;
|
||||
paramsDense.insert(ParametersPair(Parameters::kBayesPredictionLC(), kPredictionSumBelowOne));
|
||||
paramsDense.insert(ParametersPair(Parameters::kBayesSparsePrediction(), "false"));
|
||||
|
||||
ParametersMap paramsSparse = paramsDense;
|
||||
paramsSparse[Parameters::kBayesSparsePrediction()] = "true";
|
||||
|
||||
const std::vector<int> ids = getBayesIds();
|
||||
std::map<int, float> likelihood = uniformLikelihood(ids);
|
||||
|
||||
BayesFilter filterDense(paramsDense);
|
||||
BayesFilter filterSparse(paramsSparse);
|
||||
filterDense.computePosterior(memory_, likelihood);
|
||||
filterSparse.computePosterior(memory_, likelihood);
|
||||
|
||||
ASSERT_EQ(filterDense.getPosterior().size(), filterSparse.getPosterior().size());
|
||||
for(size_t i = 0; i < ids.size(); ++i)
|
||||
{
|
||||
EXPECT_NEAR(filterDense.getPosterior().at(ids[i]),
|
||||
filterSparse.getPosterior().at(ids[i]), 1e-5f) << "id=" << ids[i];
|
||||
}
|
||||
|
||||
// Falling back means the sparse view was left empty, so no extra memory is used.
|
||||
EXPECT_EQ(filterDense.getMemoryUsed(), filterSparse.getMemoryUsed());
|
||||
}
|
||||
|
||||
// The sparse view holds only the non-zero values of the prediction matrix, so it is a
|
||||
// fraction of its size, and the reported memory reflects that it is an addition to it.
|
||||
TEST_F(BayesFilterMemoryFixture, SparsePredictionMemoryUsed)
|
||||
{
|
||||
addChain(40);
|
||||
|
||||
ParametersMap params;
|
||||
params.insert(ParametersPair(Parameters::kBayesPredictionLC(), kPredictionNewPlace10Stay50Neighbor25_15));
|
||||
params.insert(ParametersPair(Parameters::kBayesSparsePrediction(), "false"));
|
||||
BayesFilter filterDense(params);
|
||||
|
||||
params[Parameters::kBayesSparsePrediction()] = "true";
|
||||
BayesFilter filterSparse(params);
|
||||
|
||||
const std::vector<int> ids = getBayesIds();
|
||||
const std::map<int, float> likelihood = uniformLikelihood(ids);
|
||||
filterDense.computePosterior(memory_, likelihood);
|
||||
filterSparse.computePosterior(memory_, likelihood);
|
||||
|
||||
const unsigned long dense = filterDense.getMemoryUsed();
|
||||
const unsigned long sparse = filterSparse.getMemoryUsed();
|
||||
EXPECT_GT(sparse, dense);
|
||||
// The matrix is ids x ids floats; a column of it only holds the neighbors within
|
||||
// the depth of the model (3 here), so the view has to stay well under it.
|
||||
EXPECT_LT(sparse - dense, ids.size()*ids.size()*sizeof(float));
|
||||
}
|
||||
|
||||
// Where the sparse and dense multiplications have to agree exactly rather than within
|
||||
// the rounding of their sums: after an iteration whose likelihood is zero everywhere
|
||||
// but on one location, the posterior is 1 there and 0 elsewhere, so the next prior is
|
||||
// one column of the prediction matrix, each of its values the result of a single
|
||||
// product. Both must then return the very same floats, which is only true if the
|
||||
// sparse view holds the values of the matrix at the same rows and columns.
|
||||
TEST_F(BayesFilterMemoryFixture, SparsePredictionIsExactOnASingleColumn)
|
||||
{
|
||||
addChain(30);
|
||||
|
||||
ParametersMap paramsDense;
|
||||
paramsDense.insert(ParametersPair(Parameters::kBayesPredictionLC(), kPredictionNewPlace10Stay50Neighbor25_15));
|
||||
paramsDense.insert(ParametersPair(Parameters::kBayesSparsePrediction(), "false"));
|
||||
|
||||
ParametersMap paramsSparse = paramsDense;
|
||||
paramsSparse[Parameters::kBayesSparsePrediction()] = "true";
|
||||
|
||||
const std::vector<int> ids = getBayesIds();
|
||||
ASSERT_GT(ids.size(), 4u);
|
||||
|
||||
// Every column of the matrix, so that a value at the wrong row or column cannot be
|
||||
// missed, whichever one it is.
|
||||
for(size_t selected = 0; selected < ids.size(); ++selected)
|
||||
{
|
||||
BayesFilter filterDense(paramsDense);
|
||||
BayesFilter filterSparse(paramsSparse);
|
||||
|
||||
std::map<int, float> oneHot;
|
||||
for(size_t i = 0; i < ids.size(); ++i)
|
||||
{
|
||||
oneHot.insert(std::make_pair(ids[i], i == selected ? 1.0f : 0.0f));
|
||||
}
|
||||
filterDense.computePosterior(memory_, oneHot);
|
||||
filterSparse.computePosterior(memory_, oneHot);
|
||||
ASSERT_FLOAT_EQ(filterDense.getPosterior().at(ids[selected]), 1.0f) << "column=" << selected;
|
||||
|
||||
const std::map<int, float> uniform = uniformLikelihood(ids);
|
||||
filterDense.computePosterior(memory_, uniform);
|
||||
filterSparse.computePosterior(memory_, uniform);
|
||||
|
||||
for(size_t i = 0; i < ids.size(); ++i)
|
||||
{
|
||||
EXPECT_FLOAT_EQ(filterDense.getPosterior().at(ids[i]),
|
||||
filterSparse.getPosterior().at(ids[i]))
|
||||
<< "column=" << selected << " id=" << ids[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In localization mode the prediction is built in its sparse form directly, the matrix
|
||||
// never being allocated, which is a second implementation of the same probabilities: it
|
||||
// has to give what the matrix gives. Compared over a fixed graph, on which the
|
||||
// prediction is generated once and kept.
|
||||
TEST_F(BayesFilterMemoryFixture, CompareSparsePredictionModesInLocalizationMode)
|
||||
{
|
||||
addChain(30);
|
||||
switchToLocalization();
|
||||
|
||||
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();
|
||||
ASSERT_GT(ids.size(), 4u);
|
||||
|
||||
for(int iter = 0; iter < 8; ++iter)
|
||||
{
|
||||
std::map<int, float> likelihood = uniformLikelihood(ids);
|
||||
// Favor a different location on each iteration, so the posterior moves.
|
||||
likelihood[ids[1 + (size_t)iter % (ids.size()-1)]] = 4.0f;
|
||||
|
||||
filterDense.computePosterior(memory_, likelihood);
|
||||
filterSparse.computePosterior(memory_, likelihood);
|
||||
|
||||
ASSERT_EQ(filterDense.getPosterior().size(), filterSparse.getPosterior().size());
|
||||
for(size_t i = 0; i < ids.size(); ++i)
|
||||
{
|
||||
EXPECT_NEAR(filterDense.getPosterior().at(ids[i]),
|
||||
filterSparse.getPosterior().at(ids[i]), 1e-5f)
|
||||
<< "iter=" << iter << " id=" << ids[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The same exactness check as SparsePredictionIsExactOnASingleColumn, but against the
|
||||
// prediction built directly in its sparse form: a posterior that is 1 on one location
|
||||
// and 0 elsewhere makes each value of the prior a single product, so the two builds have
|
||||
// to return the very same floats. This is what says that the sparse build puts the same
|
||||
// probabilities at the same rows and columns as the matrix does.
|
||||
TEST_F(BayesFilterMemoryFixture, SparsePredictionInLocalizationModeIsExactOnASingleColumn)
|
||||
{
|
||||
addChain(30);
|
||||
switchToLocalization();
|
||||
|
||||
ParametersMap paramsDense;
|
||||
paramsDense.insert(ParametersPair(Parameters::kBayesPredictionLC(), kPredictionNewPlace10Stay50Neighbor25_15));
|
||||
paramsDense.insert(ParametersPair(Parameters::kBayesSparsePrediction(), "false"));
|
||||
|
||||
ParametersMap paramsSparse = paramsDense;
|
||||
paramsSparse[Parameters::kBayesSparsePrediction()] = "true";
|
||||
|
||||
const std::vector<int> ids = getBayesIds();
|
||||
ASSERT_GT(ids.size(), 4u);
|
||||
|
||||
for(size_t selected = 0; selected < ids.size(); ++selected)
|
||||
{
|
||||
BayesFilter filterDense(paramsDense);
|
||||
BayesFilter filterSparse(paramsSparse);
|
||||
|
||||
std::map<int, float> oneHot;
|
||||
for(size_t i = 0; i < ids.size(); ++i)
|
||||
{
|
||||
oneHot.insert(std::make_pair(ids[i], i == selected ? 1.0f : 0.0f));
|
||||
}
|
||||
filterDense.computePosterior(memory_, oneHot);
|
||||
filterSparse.computePosterior(memory_, oneHot);
|
||||
|
||||
const std::map<int, float> uniform = uniformLikelihood(ids);
|
||||
filterDense.computePosterior(memory_, uniform);
|
||||
filterSparse.computePosterior(memory_, uniform);
|
||||
|
||||
for(size_t i = 0; i < ids.size(); ++i)
|
||||
{
|
||||
EXPECT_FLOAT_EQ(filterDense.getPosterior().at(ids[i]),
|
||||
filterSparse.getPosterior().at(ids[i]))
|
||||
<< "column=" << selected << " id=" << ids[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// What the sparse build is for: the prediction matrix, which is the size of the working
|
||||
// memory squared, is not allocated at all.
|
||||
TEST_F(BayesFilterMemoryFixture, SparsePredictionInLocalizationModeDoesNotAllocateTheMatrix)
|
||||
{
|
||||
addChain(200);
|
||||
switchToLocalization();
|
||||
|
||||
ParametersMap params;
|
||||
params.insert(ParametersPair(Parameters::kBayesPredictionLC(), kPredictionNewPlace10Stay50Neighbor25_15));
|
||||
params.insert(ParametersPair(Parameters::kBayesSparsePrediction(), "false"));
|
||||
BayesFilter filterDense(params);
|
||||
|
||||
params[Parameters::kBayesSparsePrediction()] = "true";
|
||||
BayesFilter filterSparse(params);
|
||||
|
||||
const std::vector<int> ids = getBayesIds();
|
||||
const std::map<int, float> likelihood = uniformLikelihood(ids);
|
||||
filterDense.computePosterior(memory_, likelihood);
|
||||
filterSparse.computePosterior(memory_, likelihood);
|
||||
|
||||
const unsigned long matrix = ids.size()*ids.size()*sizeof(float);
|
||||
EXPECT_GT(filterDense.getMemoryUsed(), matrix);
|
||||
// Not the matrix, and not the neighborhood of every location either, which only the
|
||||
// incremental update of the matrix needs.
|
||||
EXPECT_LT(filterSparse.getMemoryUsed(), filterDense.getMemoryUsed()/4);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user