GMMParameterEstimator.cpp
Go to the documentation of this file.
1 
19 
21 
22  GMMParameterEstimator::GMMParameterEstimator(unsigned int pNumberDimensions, unsigned int pNumberKernelsMin, unsigned int pNumberKernelsMax, unsigned int pNumberOfRuns, unsigned int pNumberOfSyntheticSamples, double pIntervalPosition, double pIntervalOrientation, std::string pPathOrientationPlots, unsigned int pAttemptsPerRun)
23  : mNumberDimensions(pNumberDimensions)
24  , mNumberKernelsMin(pNumberKernelsMin)
25  , mNumberKernelsMax(pNumberKernelsMax)
26  , mNumberKernels(pNumberKernelsMax - pNumberKernelsMin + 1)
27  , mNumberOfRuns(pNumberOfRuns)
28  , mNumberOfSyntheticSamples(pNumberOfSyntheticSamples)
29  , mIntervalPosition(pIntervalPosition)
30  , mIntervalOrientation(pIntervalOrientation)
31  , mPathOrientationPlots(pPathOrientationPlots)
32  , mAttemptsPerRun(pAttemptsPerRun)
33  {
34  }
35 
37  {
38  }
39 
40  void GMMParameterEstimator::addDatum(Eigen::Vector3d pSample)
41  {
42  // Check, if leaner was properly configured.
43  if(mNumberDimensions != 3)
44  throw std::invalid_argument("Cannot add a 3d sample to a learner configured for " + boost::lexical_cast<std::string>(mNumberDimensions) + "d samples.");
45 
46  // Add the original sample.
47  std::vector<double> datum;
48  datum.push_back(pSample[0]);
49  datum.push_back(pSample[1]);
50  datum.push_back(pSample[2]);
51  mData.push_back(datum);
52 
54  {
55  // Generate random generators for all three axes.
56  std::random_device rd;
57  std::default_random_engine generator(rd());
58  std::uniform_real_distribution<double> distribution(-mIntervalPosition, mIntervalPosition);
59 
60  // Generate a number of random samples distributed around the given seed sample.
61  for(unsigned int i = 0; i < mNumberOfSyntheticSamples; i++)
62  {
63  std::vector<double> datum;
64 
65  datum.push_back(pSample[0] + distribution(generator));
66  datum.push_back(pSample[1] + distribution(generator));
67  datum.push_back(pSample[2] + distribution(generator));
68 
69  mData.push_back(datum);
70  }
71  }
72  }
73 
74  void GMMParameterEstimator::addDatum(Eigen::Quaternion<double> pSample)
75  {
76  // Check, if learner was properly configured.
77  if(mNumberDimensions != 4)
78  throw std::invalid_argument("Cannot add a 4d sample to a learner configured for " + boost::lexical_cast<std::string>(mNumberDimensions) + "d samples.");
79 
80  // Add the original sample.
81  std::vector<double> datum;
82  datum.push_back(pSample.w());
83  datum.push_back(pSample.x());
84  datum.push_back(pSample.y());
85  datum.push_back(pSample.z());
86  mData.push_back(datum);
87 
89  {
90  // Extract euler angles from the given seed sample.
91  Eigen::Vector3d ea = pSample.toRotationMatrix().eulerAngles(0, 1, 2);
92 
93  // Generate random generators for all three axes.
94  std::random_device rd;
95  std::default_random_engine generator(rd());
96  std::uniform_real_distribution<double> distribution(-(mIntervalOrientation * M_PI) / 180.0, (mIntervalOrientation * M_PI) / 180.0);
97 
98  // Generate a number of random samples distributed around the given seed sample.
99  for(unsigned int i = 0; i < mNumberOfSyntheticSamples; i++)
100  {
101  std::vector<double> datum;
102 
103  // Create rotation matrix, add noise to rotation.
104  Eigen::Matrix3d m(Eigen::AngleAxisd(ea[0] + distribution(generator), Eigen::Vector3d::UnitX())
105  * Eigen::AngleAxisd(ea[1] + distribution(generator), Eigen::Vector3d::UnitY())
106  * Eigen::AngleAxisd(ea[2] + distribution(generator), Eigen::Vector3d::UnitZ()));
107 
108  // Convert rotation matrix back to quaternion and add the noised sample to the list.
109  Eigen::Quaternion<double> noisedSample(m);
110  datum.push_back(noisedSample.w());
111  datum.push_back(noisedSample.x());
112  datum.push_back(noisedSample.y());
113  datum.push_back(noisedSample.z());
114 
115  // Add datum to list.
116  mData.push_back(datum);
117  }
118  }
119  }
120 
122  {
123  // Define the containers that store the GMMS and metadata for all numbers of kernels.
124  std::vector<unsigned int> nparams;
125  std::vector<double> llk;
126  std::vector<double> bic;
127  std::vector<GaussianMixtureModel> model;
128 
129  // Reserve memory for the data structures.
130  nparams.resize(mNumberKernels * mNumberOfRuns);
131  llk.resize(mNumberKernels * mNumberOfRuns);
132  bic.resize(mNumberKernels * mNumberOfRuns);
133  model.resize(mNumberKernels * mNumberOfRuns);
134 
135  // Learn a GMM for every number of kernels up to the maximal size.
136  for(unsigned int i = 0; i < mNumberKernels; i++)
137  {
138  bool learningCycleCompleted = false;
139 
140  // generate mNumberOfRuns models and pick the best.
141  for(unsigned int run = 0; run < mNumberOfRuns; run++)
142  {
143 
144  ROS_INFO_STREAM("Learning run " << run + 1 << ".");
145 
146  bool useGenericMatrices = true;
147 
148  // Repeat this calculation as long as the expectation maximizations finds a degenerated distribution,
149  // but maximal mAttemptsPerRun (formerly 100) times.
150  for(int timer = mAttemptsPerRun; timer > 0; timer--)
151  {
152  unsigned int offset = i * mNumberOfRuns + run;
153 
154  if(runExpectationMaximization(mData,mNumberKernelsMin + i, nparams[offset], llk[offset], bic[offset], model[offset], useGenericMatrices))
155  {
156  learningCycleCompleted = true;
157  break;
158  }
159  else
160  {
161  ROS_INFO_STREAM("Training unsuccessful. Repeating cycle.");
162  if (timer == (int) (mAttemptsPerRun / 2) + 1 && mAttemptsPerRun != 1) // at halfway point: switch to less precise but more stable diagonal matrices instead of generic ones
163  {
164  ROS_INFO_STREAM("Attempting to learn diagonal covariance matrix instead of generic one.");
165  useGenericMatrices = false;
166  }
167  }
168  }
169  }
170 
171  // Terminate if OpenCV failed to learn a proper model.
172  if(!learningCycleCompleted) {
173  ROS_ERROR("Learning cycle incomplete. GMM learning failed! Please restart learner!");
174  exit (EXIT_FAILURE);
175  } else {
176  ROS_INFO("Learning cycle completed.");
177  }
178  }
179 
180  // Determine the index of the GMM with the best BIC score.
181  unsigned int indexOfBestBic = std::max_element(bic.begin(), bic.end()) - bic.begin();
182 
183  // Information to user about the number of kernels
184  for(unsigned int i = 0; i < mNumberKernels; i++)
185  {
186  for(unsigned int run = 0; run < mNumberOfRuns; run++)
187  {
188  std::stringstream msg;
189 
190  unsigned int offset = i * mNumberOfRuns + run;
191 
192  // Mark the number of kernels with the best score.
193  if(offset == indexOfBestBic) msg << "[x] ";
194  else msg << "[ ] ";
195 
196  // Print the bic score and log likelihood.
197  msg << "Kernels: " << mNumberKernelsMin + i << " Run: " << run + 1 << " BIC: " << bic[offset] << " LLK: " << llk[offset];
198 
199  // Print message to console.
200  ROS_INFO_STREAM("" << msg.str());
201  }
202  }
203 
204  // Store the GMM in the 'mBestGMM' variable.
205  mBestGMM = model[indexOfBestBic];
206  }
207 
209  {
210  gmm = mBestGMM;
211  ROS_INFO_STREAM("Number of kernels after removing duplicates is " << mBestGMM.getNumberOfKernels() << ".");
212  }
213 
215  {
216  unsigned int numberOfKernels = mBestGMM.getNumberOfKernels();
217 
218  // Iterate over all Gaussian Kernels:
219  for (unsigned int i = 0; i < numberOfKernels; i++)
220  {
221  // Extract the mean and covariance of the kernel.
225 
226  // Prepare learner for histogramm.
227  std::vector<double> histInRoll, histInPitch, histInYaw;
228 
229  unsigned int sampleAmount = 100;
230  // Draw samples
231  std::vector<Eigen::VectorXd> samples(sampleAmount);
232  // Fill std::vector with Eigen::vectors of the correct size
233  for (unsigned int s = 0; s < sampleAmount; s++) samples.at(s) = Eigen::VectorXd(mean->size());
234  MathHelper::drawNormal(*mean, *cov, sampleAmount, samples);
235 
236  for (unsigned int i = 0; i < sampleAmount; i++)
237  {
238  Eigen::VectorXd sample = samples[i];
239 
240  // Create Eigen Quaternion
241  Eigen::Quaterniond quat = Eigen::Quaterniond(sample[0], sample[1], sample[2], sample[3]);
242 
243  // Extract euler angles from the given seed sample.
244  Eigen::Vector3d ea = quat.toRotationMatrix().eulerAngles(0, 1, 2);
245 
246  // Push negative values into interval.
247  if(ea[0] < -M_PI) ea[0] += M_PI;
248  if(ea[1] < -M_PI) ea[1] += M_PI;
249  if(ea[2] < -M_PI) ea[2] += M_PI;
250 
251  if(ea[0] > M_PI) ea[0] -= M_PI;
252  if(ea[1] > M_PI) ea[1] -= M_PI;
253  if(ea[2] > M_PI) ea[2] -= M_PI;
254 
255  // Add sample to input of cv::calcHist
256  histInRoll.push_back(ea[0]);
257  histInPitch.push_back(ea[1]);
258  histInYaw.push_back(ea[2]);
259  }
260  // Get current timestamp.
261  auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
262 
263  // Get and plot distributions.
264  // Calculate histograms by hand:
265  double lower = -M_PI; // lower limit
266  double upper = M_PI; // upper limit
267  double buckets = sampleAmount; // amount of buckets
268 
269  std::vector<std::pair<double, double>> histOutRoll(buckets), histOutPitch(buckets), histOutYaw(buckets);
270  MathHelper::calcHistogram(lower, upper, buckets, histInRoll, histOutRoll);
271  MathHelper::calcHistogram(lower, upper, buckets, histInPitch, histOutPitch);
272  MathHelper::calcHistogram(lower, upper, buckets, histInYaw, histOutYaw);
273 
274  // scale buckets into probabilities
275  for (unsigned int i = 0; i < histOutRoll.size(); i++) histOutRoll.at(i).second /= histOutRoll.size();
276  for (unsigned int i = 0; i < histOutPitch.size(); i++) histOutPitch.at(i).second /= histOutPitch.size();
277  for (unsigned int i = 0; i < histOutYaw.size(); i++) histOutYaw.at(i).second /= histOutYaw.size();
278 
279  // plot them into the gnuplot files and on screen
280  Visualization::GMMGnuplotVisualization orientationVisualizer;
281  orientationVisualizer.plotOrientationHistogram(mPathOrientationPlots + "/" + std::to_string(timestamp) + "-roll.gp", histOutRoll, "roll");
282  orientationVisualizer.plotOrientationHistogram(mPathOrientationPlots + "/" + std::to_string(timestamp) + "-pitch.gp", histOutPitch, "pitch");
283  orientationVisualizer.plotOrientationHistogram(mPathOrientationPlots + "/" + std::to_string(timestamp) + "-yaw.gp", histOutYaw, "yaw");
284  }
285  }
286 
287  bool GMMParameterEstimator::runExpectationMaximization(const std::vector<std::vector<double>> data,
288  unsigned int nc,
289  unsigned int& nparams,
290  double& llk,
291  double& bic,
292  GaussianMixtureModel& model,
293  bool useGenericMatrices)
294  {
295  int covMatType = cv::EM::COV_MAT_GENERIC; // by default, use a generic covariance matrix
296  // If there is little data, a less precise diagonal matrix is more stable as a covariance matrix:
297  if (!useGenericMatrices)
298  covMatType = cv::EM::COV_MAT_DIAGONAL; // diagonal covariance matrix
299 
300  // Creating the EM learner instance
301  cv::EM myEM = cv::EM(nc, covMatType,
302  cv::TermCriteria( // termination criteria
303  cv::TermCriteria::COUNT + cv::TermCriteria::EPS, // use maximum iterations and convergence epsilon
304  10, // number of maximum iterations. 10 was used in ProBT.
305  0.01 // epsilon. 0.01 was used in ProBT.
306  )
307  );
308 
309  cv::Mat loglikelihoods;
310  // Put data in cv::Mat
311  cv::Mat cvdata(data.size(), data.at(0).size(), CV_64FC1);
312  for (int i = 0; i < cvdata.rows; i++)
313  for(int j = 0; j < cvdata.cols; j++)
314  cvdata.at<double>(i, j) = data.at(i).at(j);
315 
316  // Run until convergence
317  if (!myEM.train(cvdata, loglikelihoods)) return false; // Training failed: return false
318 
319  // extract covariance matrices and check whether they are symmetric and positive semi-definite, i.e. whether they actually are valid covariance matrices:
320  std::vector<cv::Mat> cvcovs = myEM.get<std::vector<cv::Mat>>("covs");
321  std::vector<boost::shared_ptr<Eigen::MatrixXd>> covs(cvcovs.size());
322  for (unsigned int i = 0; i < cvcovs.size(); i++)
323  {
324  cv::Mat cvcov = cvcovs.at(i);
325  // Check whether matrix is quadratic:
326  if (cvcov.rows != cvcov.cols)
327  {
328  ROS_DEBUG_STREAM("Matrix not quadratic: not a valid covariance matrix");
329  return false;
330  }
331  Eigen::MatrixXd cov(cvcov.rows, cvcov.cols);
332  // Check symmetry of matrix:
333  for (int j = 0; j < cvcov.rows; j++)
334  {
335  for (int k = j; k < cvcov.cols; k++)
336  {
337  double entry = cvcov.at<double>(j,k);
338  if (j != k) // if not on diagonal:
339  {
340  if(std::abs(entry - cvcov.at<double>(k,j)) >= std::numeric_limits<double>::epsilon()) // check symmetry
341  {
342  ROS_DEBUG_STREAM("Found unsymmetric covariance matrix entries " << cvcov.at<double>(j,k) << " (" << j << "," << k << "), "
343  << cvcov.at<double>(k,j) << " (" << k << "," << j << "). Not a valid covariance matrix.");
344  return false; // Not symmetric: return false
345  }
346  cov(k,j) = entry; // if symmetric: set lower entry
347  }
348  cov(j,k) = entry; // set upper entry or entry on diagonal
349  }
350  }
351 
352  // Check positive semi-definiteness by checking whether all eigenvalues are positive:
353  Eigen::MatrixXd eigenvalues;
354  if (cov.rows() == 3) // dimension 3
355  {
356  Eigen::SelfAdjointEigenSolver<Eigen::Matrix3d> es(cov);
357  eigenvalues = es.eigenvalues();
358 
359  }
360  else if (cov.rows() == 4)
361  {
362  Eigen::SelfAdjointEigenSolver<Eigen::Matrix4d> es(cov);
363  eigenvalues = es.eigenvalues();
364  }
365  else
366  throw std::invalid_argument("In GMMParameterEstimator::runExpectationMaximization(): Found matrix with unsupported number of dimensions (supported are 3,4).");
367 
368  for (unsigned int i = 0; i < eigenvalues.size(); i++)
369  {
370  if (eigenvalues(i) < 0)
371  {
372  ROS_DEBUG_STREAM("Found invalid eigenvalue " << eigenvalues(i) << ": matrix not positive semi-definite, not a valid covariance matrix.");
373  return false; // Not positive semi-definite: return false
374  }
375  else if (eigenvalues(i) < std::numeric_limits<double>::epsilon())
376  {
377  ROS_DEBUG_STREAM("Found eigenvalue 0: Matrix cannot be inverted, invalid.");
378  return false; // matrix not invertible: return false (is a valid covariance matrix, but cannot be used in inference).
379  }
380  }
381 
382  covs.at(i) = boost::shared_ptr<Eigen::MatrixXd>(new Eigen::MatrixXd(cov)); // only if matrix is valid: put it into list.
383  }
384 
385  // Fill the output parameters
386  llk = 0;
387  for (int i = 0; i < loglikelihoods.rows; i++) llk += loglikelihoods.at<double>(i,0); // llk is sum of the loglikelihoods for each sample
388 
389  // model:
390  // extract weights
391  cv::Mat cvweights = myEM.get<cv::Mat>("weights");
392  std::vector<double> weights(cvweights.cols);
393  for (int i = 0; i < cvweights.cols; i++) weights.at(i) = cvweights.at<double>(i);
394  // extract means
395  cv::Mat cvmeans = myEM.get<cv::Mat>("means");
396  std::vector<boost::shared_ptr<Eigen::VectorXd>> means(cvmeans.rows);
397  for (int i = 0; i < cvmeans.rows; i++)
398  {
399  Eigen::VectorXd mean(cvmeans.cols);
400  for (int j = 0; j < cvmeans.cols; j++) mean[j] = cvmeans.at<double>(i, j);
401  means.at(i) = boost::shared_ptr<Eigen::VectorXd>(new Eigen::VectorXd(mean));
402  }
403 
405  // Iterate over all gaussian kernels and save them.
406  for(unsigned int i = 0; i < weights.size(); i++)
407  {
408  PSMLearner::GaussianKernel kernel; // Create a new gaussian kernel.
409  kernel.mWeight = weights[i]; // Set the weight.
410  kernel.mMean = means[i]; // Set the mean vector.
411  kernel.mCovariance = covs[i]; // Set the covariance matrix.
412 
413  newmodel.addKernel(kernel); // Add the kernel to the GMM.
414  }
415  // We removed kernels (automatically in addKernel), so we have to normalize the weights
416  newmodel.normalizeWeights();
417  model = newmodel;
418 
419  // number of independent parameters nparams (for generic covariance matrix):
420  nparams = (model.getKernels().size() - 1);
421  if (!model.getKernels().empty()) {
422  unsigned int d = model.getKernels().at(0).mMean->size(); // dimension of mean vector and also symmetric covariance matrix
423  if (useGenericMatrices)
424  nparams += (d * (d + 1) / 2 + d) * model.getKernels().size(); // (free variables in covariance matrix + in mean) * number of kernels + number of weights - 1
425  else
426  nparams += 2 * d * model.getKernels().size(); // (free variables on diagonal of covariance matrix + in mean) * number of kernels + number of weights - 1
427  // weights are parameters too, but if there is only one kernel, it's not independent (since it has to be 1)
428  // one weight is also always determined by the rest, since the sum has to be 1.
429  }
430  else
431  throw std::runtime_error("In GMMParameterEstimator::runExpectationMaximization(): trying to use empty model.");
432 
433  // Bayesian information criterion BIC:
434  bic = llk - 0.25 * (double) nparams * std::log((double) data.size());
435 
436  return true; // Training succeeded: return true
437  }
438 }
d
bool runExpectationMaximization(const std::vector< std::vector< double >> data, unsigned int nc, unsigned int &nparams, double &llk, double &bic, GaussianMixtureModel &model, bool useGenericMatrices=true)
static void drawNormal(const Eigen::VectorXd &mean, const Eigen::MatrixXd &cov, unsigned int amount, std::vector< Eigen::VectorXd > &samples)
Definition: MathHelper.cpp:31
XmlRpcServer s
std::vector< PSMLearner::GaussianKernel > getKernels() const
GMMParameterEstimator(unsigned int pNumberOfDimensions, unsigned int pNumberOfKernelsMin, unsigned int pNumberOfKernelsMax, unsigned int pNumberOfRuns, unsigned int pNumberOfSyntheticSamples, double pIntervalPosition, double pIntervalOrientation, std::string pPathOrientationPlots, unsigned int pAttemptsPerKernel)
#define ROS_INFO(...)
#define ROS_DEBUG_STREAM(args)
#define ROS_INFO_STREAM(args)
#define ROS_ERROR(...)
static void calcHistogram(double lower, double upper, unsigned int buckets, std::vector< double > in, std::vector< std::pair< double, double >> &out)
Definition: MathHelper.cpp:45


asr_psm
Author(s): Braun Kai, Gehrung Joachim, Heizmann Heinrich, Meißner Pascal
autogenerated on Fri Nov 15 2019 03:57:54