18#include <Eigen/Cholesky>
49template <
typename Scalar>
73template <
typename Space,
typename Scalar =
double,
typename Allocator = DefaultAllocator>
86template <
typename Scalar,
typename Space>
91 Eigen::Array<Scalar, Eigen::Dynamic, 1> weights;
92 std::vector<MatrixType> covs;
93 std::vector<MatrixType> covFactors;
98template <
typename Scalar,
typename Space,
typename Allocator>
99[[nodiscard]] std::expected<FitSource<Scalar, Space>,
FitError>
100fitSource(ParticleSet<Space, Scalar, Allocator>
const & particles)
102 auto weights = particles.normalizedWeights();
107 return FitSource<Scalar, Space>{particles.states(), std::move(*weights), {}, {}, particles.space()};
113template <
typename Scalar,
typename Space,
typename Allocator>
114[[nodiscard]] FitSource<Scalar, Space>
117 using MatrixType =
typename FitSource<Scalar, Space>::MatrixType;
118 auto source = FitSource<Scalar, Space>{
120 Eigen::Array<Scalar, Eigen::Dynamic, 1>{mixture.size()},
125 source.covs.reserve(std::size_t(mixture.size()));
126 source.covFactors.reserve(std::size_t(mixture.size()));
128 for (
auto const & entry : mixture)
130 auto const gaussian = entry.component.toDensity();
131 source.points.col(i) = gaussian->mean();
132 source.weights(i) = Eigen::numext::exp(entry.logWeight - mixture.logTotalWeight());
133 source.covs.push_back(MatrixType{entry.component.covarianceLower().template selfadjointView<Eigen::Lower>()});
134 source.covFactors.push_back(MatrixType{gaussian->llt().matrixL()});
156template <
typename Scalar,
typename Space,
typename Allocator>
161 using MomentsType = Moments<Space, Scalar>;
167 using Weights = Eigen::Array<Scalar, Eigen::Dynamic, 1>;
168 using Responsibilities = Eigen::Array<Scalar, Eigen::Dynamic, Eigen::Dynamic>;
169 using Source = FitSource<Scalar, Space>;
170 using Result = MixtureFit<Space, Scalar, Allocator>;
173 MixtureFitter(Source
const & source, FitOptions<Scalar>
const & options, Allocator
const & allocator)
176 , m_allocator{allocator}
178 EMRYS_FILTER_ASSERT(options.maxIterations >= 1);
182 template <std::uniform_random_bit_generator G>
183 [[nodiscard]] std::expected<Result, FitError>
184 run(Points
const & candidates, Weights
const & candidateWeights, Eigen::Index k, G & rng)
193 template <ExecutionPolicy P, std::uniform_random_bit_generator G>
194 [[nodiscard]] std::expected<Result, FitError>
195 run(P && policy, Points
const & candidates, Weights
const & candidateWeights, Eigen::Index k, G & rng)
197 EMRYS_FILTER_ASSERT(k >= 1);
198 if (!prepareWhitening())
202 Responsibilities responsibilities = initialAssignment(seed(candidates, candidateWeights, k, rng));
203 mStep(policy, responsibilities);
205 Scalar previous = -Eigen::NumTraits<Scalar>::infinity();
206 for (
int iteration = 1;; ++iteration)
213 bool const converged = *
logLikelihood - previous <= m_options.tolerance;
214 if (converged || iteration >= m_options.maxIterations)
219 mStep(policy, responsibilities);
229 MomentsType
const moments =
230 weightedMomentsIn<MomentsType>(m_source.space, m_source.points, m_source.weights.log(), Scalar(0));
231 MatrixType total = moments.covarianceLower();
232 for (std::size_t i = 0; i < m_source.covs.size(); ++i)
234 total += m_source.weights(Eigen::Index(i)) * m_source.covs[i];
236 DifferenceVectorType
const variances = total.diagonal();
240 DifferenceVectorType
const secondMoments = [&] {
241 if constexpr (hasEqualSizes<Space>)
243 return DifferenceVectorType{variances.array() + moments.mean().array().square()};
250 if (!(variances.array() > Eigen::NumTraits<Scalar>::epsilon() * secondMoments.array()).all()
255 m_floor = m_options.regularization * variances;
256 total.diagonal() += m_floor;
257 m_whitening.compute(total);
258 return m_whitening.info() == Eigen::Success;
264 template <
typename Derived>
265 [[nodiscard]] Weights
266 distancesFrom(Points
const & points, Eigen::MatrixBase<Derived>
const & center)
const
268 Deviations differences =
Emrys::minus(m_source.space, points, center);
269 solveInPlace(m_whitening.matrixL(), differences);
270 return differences.colwise().squaredNorm().transpose().array();
280 template <
typename G>
282 seed(Points
const & candidates, Weights
const & weights, Eigen::Index k, G & rng)
const
284 Eigen::Index
const m = candidates.cols();
285 Weights distances = Weights::Constant(m, Eigen::NumTraits<Scalar>::infinity());
286 auto probabilities = std::vector<Scalar>(weights.begin(), weights.end());
287 auto chosen = std::vector<Eigen::Index>{};
290 auto pick = std::array<Eigen::Index, 1>{};
291 Scalar
const u = uniform01<Scalar>(rng);
292 invertCumulative(std::span<Scalar const>{probabilities}, std::span<Eigen::Index>{pick}, [u](std::size_t) {
295 chosen.push_back(pick[0]);
296 if (Eigen::Index(chosen.size()) == k)
300 distances = distances.min(distancesFrom(candidates, candidates.col(pick[0])));
302 for (Eigen::Index i = 0; i < m; ++i)
304 probabilities[std::size_t(i)] = weights(i) * distances(i);
305 total += probabilities[std::size_t(i)];
307 if (!(total > Scalar(0)))
312 auto seeds = Points{candidates.rows(), Eigen::Index(chosen.size())};
313 for (std::size_t s = 0; s < chosen.size(); ++s)
315 seeds.col(Eigen::Index(s)) = candidates.col(chosen[s]);
321 [[nodiscard]] Responsibilities
322 initialAssignment(Points
const & seeds)
const
324 Eigen::Index
const n = m_source.points.cols();
325 Weights nearestDistance = Weights::Constant(n, Eigen::NumTraits<Scalar>::infinity());
326 Eigen::Array<Eigen::Index, Eigen::Dynamic, 1> nearest = Eigen::Array<Eigen::Index, Eigen::Dynamic, 1>::Zero(n);
327 for (Eigen::Index s = 0; s < seeds.cols(); ++s)
329 Weights
const distances = distancesFrom(m_source.points, seeds.col(s));
330 for (Eigen::Index i = 0; i < n; ++i)
332 if (distances(i) < nearestDistance(i))
334 nearestDistance(i) = distances(i);
339 Responsibilities responsibilities = Responsibilities::Zero(seeds.cols(), n);
340 for (Eigen::Index i = 0; i < n; ++i)
342 responsibilities(nearest(i), i) = Scalar(1);
344 return responsibilities;
350 template <
typename P>
352 mStep(P && policy, Responsibilities
const & responsibilities)
354 m_logWeights.clear();
357 for (Eigen::Index k = 0; k < responsibilities.rows(); ++k)
359 Weights
const w = m_source.weights * responsibilities.row(k).transpose();
360 Scalar
const mass = w.sum();
361 if (!(mass > Scalar(0)) || Eigen::numext::log(mass) < m_options.minLogWeight)
365 MomentsType moments = weightedMomentsIn<MomentsType>(
370 Eigen::numext::log(mass)
373 moments.covarianceLower() = reduceRanges(
375 Eigen::Index(m_source.covs.size()),
376 [&](Eigen::Index begin, Eigen::Index length) {
379 ? moments.covarianceLower()
380 : MatrixType::Zero(moments.covarianceLower().rows(), moments.covarianceLower().cols());
381 for (Eigen::Index i = begin; i < begin + length; ++i)
383 if (w(i) > Scalar(0))
385 sum += (w(i) / mass) * m_source.covs[std::size_t(i)];
390 [](MatrixType a, MatrixType
const & b) {
395 moments.covarianceLower().diagonal() += m_floor;
396 m_logWeights.push_back(Eigen::numext::log(mass));
397 m_means.push_back(moments.mean());
398 m_covs.push_back(moments.covarianceLower());
408 template <
typename P>
409 [[nodiscard]] std::expected<Scalar, FitError>
410 eStep(P && policy, Responsibilities & responsibilities)
412 auto factors = std::vector<Eigen::LLT<MatrixType>>{};
413 auto logDeterminants = std::vector<Scalar>{};
414 factors.reserve(m_covs.size());
415 logDeterminants.reserve(m_covs.size());
417 for (std::size_t k = 0; k < m_covs.size(); ++k)
419 auto llt = Eigen::LLT<MatrixType>{m_covs[k]};
420 Scalar
const logDeterminant = Scalar(2) * llt.matrixLLT().diagonal().array().log().sum();
421 if (llt.info() != Eigen::Success || !
isFinite(logDeterminant))
425 m_logWeights[out] = m_logWeights[k];
426 m_means[out] = m_means[k];
427 m_covs[out] = m_covs[k];
428 factors.push_back(std::move(llt));
429 logDeterminants.push_back(logDeterminant);
432 m_logWeights.resize(out);
440 Eigen::Index
const n = m_source.points.cols();
443 auto logTerms = Responsibilities{Eigen::Index(out), n};
444 Eigen::Index
const withCovariance = Eigen::Index(m_source.covFactors.size());
446 forEachChunk(policy, n, [&](Eigen::Index begin, Eigen::Index length) {
447 for (std::size_t k = 0; k < out; ++k)
449 Deviations residuals =
450 Emrys::minus(m_source.space, m_source.points.middleCols(begin, length), m_means[k]);
451 solveInPlace(factors[k].matrixL(), residuals);
452 logTerms.row(Eigen::Index(k)).segment(begin, length) =
453 m_logWeights[k] - Scalar(0.5) * (dimension *
log2Pi + logDeterminants[k])
454 - Scalar(0.5) * residuals.colwise().squaredNorm().array();
455 for (Eigen::Index i = begin; i < std::min(begin + length, withCovariance); ++i)
457 MatrixType solved = m_source.covFactors[std::size_t(i)];
458 solveInPlace(factors[k].matrixL(), solved);
459 logTerms(Eigen::Index(k), i) -= Scalar(0.5) * solved.squaredNorm();
464 responsibilities.resize(logTerms.rows(), n);
465 auto contributions = Weights{n};
466 forEachIndex(policy, n, [&](Eigen::Index i) {
467 auto sum = LogSumExp<Scalar>{};
468 for (Eigen::Index k = 0; k < logTerms.rows(); ++k)
470 sum.add(logTerms(k, i));
472 Scalar
const logNormalizer = sum.value();
474 if (!
isFinite(logNormalizer) || !(m_source.weights(i) > Scalar(0)))
476 responsibilities.col(i).setZero();
477 contributions(i) = Scalar(0);
480 responsibilities.col(i) = (logTerms.col(i) - logNormalizer).exp();
481 contributions(i) = m_source.weights(i) * logNormalizer;
484 Scalar logLikelihood = 0;
485 for (Scalar
const contribution : contributions)
487 logLikelihood += contribution;
489 return logLikelihood;
494 result(Scalar logLikelihood,
int iterations,
bool converged)
const
497 mixture.
reserve(Eigen::Index(m_means.size()));
498 for (std::size_t k = 0; k < m_means.size(); ++k)
500 mixture.add(m_logWeights[k], m_means[k], m_covs[k]);
502 return {std::move(mixture), logLikelihood, iterations, converged};
505 Source
const & m_source;
506 FitOptions<Scalar> m_options;
507 Allocator m_allocator;
508 Eigen::LLT<MatrixType> m_whitening;
509 DifferenceVectorType m_floor;
510 std::vector<Scalar> m_logWeights;
511 std::vector<VectorType> m_means;
512 std::vector<MatrixType> m_covs;
530template <
typename Scalar,
typename Space,
typename Allocator, std::uniform_random_bit_generator G>
531[[nodiscard]] std::expected<MixtureFit<Space, Scalar, Allocator>,
FitError>
539 auto const source = internal::fitSource(particles);
542 return std::unexpected{source.error()};
544 auto fitter = internal::MixtureFitter<Scalar, Space, Allocator>{*source, options, particles.
get_allocator()};
545 return fitter.run(source->points, source->weights, k, rng);
559template <
typename Scalar,
typename Space,
typename Allocator, std::uniform_random_bit_generator G>
560[[nodiscard]] std::expected<MixtureFit<Space, Scalar, Allocator>,
FitError>
568 auto const source = internal::fitSource(mixture);
569 auto fitter = internal::MixtureFitter<Scalar, Space, Allocator>{source, options, mixture.
get_allocator()};
570 return fitter.run(source.points, source.weights, k, rng);
586 typename SeedAllocator,
587 std::uniform_random_bit_generator G>
588[[nodiscard]] std::expected<MixtureFit<Space, Scalar, Allocator>,
FitError>
597 EMRYS_FILTER_ASSERT(particles.
space() == seedsFrom.
space() &&
"the particles and seeds must share a space");
598 auto const source = internal::fitSource(particles);
601 return std::unexpected{source.error()};
603 auto const seeds = internal::fitSource(seedsFrom);
604 auto fitter = internal::MixtureFitter<Scalar, Space, Allocator>{*source, options, particles.
get_allocator()};
605 return fitter.run(seeds.points, seeds.weights, k, rng);
613template <ExecutionPolicy P,
typename Scalar,
typename Space,
typename Allocator, std::uniform_random_bit_generator G>
614[[nodiscard]] std::expected<MixtureFit<Space, Scalar, Allocator>,
FitError>
623 auto const source = internal::fitSource(particles);
626 return std::unexpected{source.error()};
628 auto fitter = internal::MixtureFitter<Scalar, Space, Allocator>{*source, options, particles.
get_allocator()};
629 return fitter.run(std::forward<P>(policy), source->points, source->weights, k, rng);
636template <ExecutionPolicy P,
typename Scalar,
typename Space,
typename Allocator, std::uniform_random_bit_generator G>
637[[nodiscard]] std::expected<MixtureFit<Space, Scalar, Allocator>,
FitError>
646 auto const source = internal::fitSource(mixture);
647 auto fitter = internal::MixtureFitter<Scalar, Space, Allocator>{source, options, mixture.
get_allocator()};
648 return fitter.run(std::forward<P>(policy), source.points, source.weights, k, rng);
660 typename SeedAllocator,
661 std::uniform_random_bit_generator G>
662[[nodiscard]] std::expected<MixtureFit<Space, Scalar, Allocator>,
FitError>
672 EMRYS_FILTER_ASSERT(particles.
space() == seedsFrom.
space() &&
"the particles and seeds must share a space");
673 auto const source = internal::fitSource(particles);
676 return std::unexpected{source.error()};
678 auto const seeds = internal::fitSource(seedsFrom);
679 auto fitter = internal::MixtureFitter<Scalar, Space, Allocator>{*source, options, particles.
get_allocator()};
680 return fitter.run(std::forward<P>(policy), seeds.points, seeds.weights, k, rng);
685#ifndef EMRYS_FILTER_NO_IO
695describe(std::ostream & stream,
FitError error, Indent indent)
701 stream <<
"the source has no spread in some direction";
704 stream <<
"the weights cannot be normalized";
Execution policies (decisions.md [execution-policies]): execution::sequential, the ExecutionPolicy co...
The macros every header includes first.
Mathematical constants and scalar functions that work in every scalar type the library accepts,...
The mixture: the parameters MixtureParams of weighted components of one type (decisions....
Moments: the mean and covariance that a Gaussian carries and that a mixture or a sample set is moment...
The particle set: ParticleSet, the weighted samples, with resampling, upkeep of weights and particles...
Printing: operator<< and std::formatter for every distribution, every filter (which prints its name a...
The resamplers: function objects that draw ancestors in proportion to weights, which ParticleSet::res...
SampleSet: a matrix whose columns are samples in a space, which the samplers return and the unscented...
Solving by a factorization of a small matrix: transposedSolve() gives , the shape of a Kalman gain an...
The Space concept and what every continuous space shares (decisions.md [sizes-and-dimensions]): the t...
Weighted statistics over points, all in internal, which the containers of weighted elements (Gaussian...
Log-weights: the WeightError a normalization can fail with (NumericError.h), and the streaming log-su...
void reserve(Index n)
Reserves room for n components without setting a capacity.
Definition Mixture.h:1080
allocator_type get_allocator() const
Definition Mixture.h:2150
SpaceType const & space() const
Definition Mixture.h:2143
DifferenceMatrix< Space_, Space_, Scalar > MatrixType
a matrix over differences, such as the covariance
Definition Moments.h:76
DifferenceVector< Space_, Scalar > DifferenceVectorType
Definition Moments.h:75
ValueVector< Space_, Scalar > VectorType
Definition Moments.h:74
A set of weighted samples: the empirical distribution.
Definition ParticleSet.h:279
allocator_type get_allocator() const
Definition ParticleSet.h:599
Space_ const & space() const
Definition ParticleSet.h:652
A type usable as the space of a distribution.
Definition Space.h:186
bool allFinite(Eigen::DenseBase< Derived > const &x)
Definition Math.h:354
bool isFinite(T const &x)
Definition Math.h:305
Scalar log2Pi()
Definition Math.h:177
Mixture< Gaussian< Space_, Scalar_ >, Allocator_ > GaussianMixture
The Mixture of a GaussianMixtureParams, the spelling for the common case (decisions....
Definition Mixture.h:2330
Eigen::Matrix< Scalar, valueDimensionAtCompileTime< Space >, Eigen::Dynamic,(maxValueDimensionAtCompileTime< Space >==1 ? Eigen::RowMajor :Eigen::ColMajor), maxValueDimensionAtCompileTime< Space >, Eigen::Dynamic > SampleSet
A matrix whose columns are samples in Space (see Spaces), with the space's size and storage bound.
Definition SampleSet.h:33
FitError
Reasons fitMixture() can fail.
Definition Fitting.h:41
MixtureParams< Gaussian< Space_, Scalar_ >, Allocator_ > GaussianMixtureParams
A Mixture of Gaussians in covariance form, the spelling for the common case (decisions....
Definition Mixture.h:2072
std::expected< MixtureFit< Space, Scalar, Allocator >, FitError > fitMixture(ParticleSet< Space, Scalar, Allocator > const &particles, Eigen::Index k, G &rng, FitOptions< Scalar > const &options={})
Definition Fitting.h:532
@ Degenerate
the source has no spread in some dimension, e.g. every particle has the same state
Definition Fitting.h:42
@ InvalidWeights
the particles' weights cannot be normalized (see ParticleSet::logTotalWeight())
Definition Fitting.h:43
constexpr Sequential sequential
The default execution policy: plain loops on the calling thread, in index order (Sequential).
Definition Execution.h:128
Derived::Scalar logLikelihood(M const &measurement, Space const &space, Eigen::MatrixBase< Derived > const &x)
Definition Likelihood.h:433
auto minus(S const &space, Eigen::MatrixBase< ADerived > const &a, Eigen::MatrixBase< BDerived > const &b)
Definition Space.h:715
Eigen::Index differenceDimension(S const &space)
Definition Space.h:284
Options for fitMixture().
Definition Fitting.h:51
int maxIterations
Maximum number of EM iterations (E-steps).
Definition Fitting.h:53
Scalar regularization
Covariance floor, relative to the source's variance in each dimension: every component covariance get...
Definition Fitting.h:64
Scalar tolerance
Stop when an iteration improves the weighted log-likelihood by at most this.
Definition Fitting.h:56
Scalar minLogWeight
Components whose normalized log-weight falls below this are dropped.
Definition Fitting.h:67
Result of fitMixture(), in Space, with the source's allocator.
Definition Fitting.h:75
int iterations
Definition Fitting.h:78
Scalar logLikelihood
Definition Fitting.h:77
GaussianMixtureParams< Space, Scalar, Allocator > mixture
Definition Fitting.h:76
bool converged
Definition Fitting.h:79