Échantillon de la distribution multivariée normale / gaussienne en C++
j'ai cherché un moyen pratique d'échantillonner à partir d'une distribution normale multivariée. Est-ce que quelqu'un connaît un extrait de code facilement disponible pour faire ça? Pour les matrices/vecteurs, je préfère utiliser Boost ou Propres ou un autre phénoménale de la bibliothèque, je ne suis pas familier avec, mais je pourrais utiliser GSL dans un pincement. J'aimerais aussi si la méthode non négatif-matrices de covariance définies plutôt que d'exiger une covariance positive-définie (par exemple, comme avec la décomposition Cholesky). Cela existe dans MATLAB, NumPy, et d'autres, mais j'ai eu du mal à trouver une solution C/C++ prête à l'emploi.
si je dois le mettre en oeuvre moi-même, je me plaindrai, mais c'est bon. Si je le fais, Wikipedia le fait sonner comme je devrais
- création n 0-moyenne, la variance, indépendant des échantillons normaux (boost va faire cet)
- trouver la décomposition propre de la covariance matrice
- échelle chacun des n des échantillons par la racine carrée de la valeur propre correspondante
- faire pivoter le vecteur des échantillons en pré-multipliant le vecteur gradué par la matrice des vecteurs propres orthonormaux trouvés par la décomposition
je voudrais que cela fonctionne rapidement. Est-ce que quelqu'un a une intuition pour quand il vaudrait la peine de vérifier pour voir si la matrice de covariance est positive, et si oui, utilisez Cholesky à la place?
3 réponses
puisque cette question a recueilli beaucoup de points de vue, j'ai pensé poster le code pour la réponse finale que j'ai trouvé, en partie, par poster sur les forums D'Eigen. Le code utilise Boost pour la normale univariée et Eigen pour la manipulation de matrice. Cela semble peu orthodoxe, car cela implique l'utilisation de l'espace de noms "interne", mais cela fonctionne. Je suis ouvert à l'améliorer si quelqu'un propose un.
#include <Eigen/Dense>
#include <boost/random/mersenne_twister.hpp>
#include <boost/random/normal_distribution.hpp>
/*
We need a functor that can pretend it's const,
but to be a good random number generator
it needs mutable state.
*/
namespace Eigen {
namespace internal {
template<typename Scalar>
struct scalar_normal_dist_op
{
static boost::mt19937 rng; // The uniform pseudo-random algorithm
mutable boost::normal_distribution<Scalar> norm; // The gaussian combinator
EIGEN_EMPTY_STRUCT_CTOR(scalar_normal_dist_op)
template<typename Index>
inline const Scalar operator() (Index, Index = 0) const { return norm(rng); }
};
template<typename Scalar> boost::mt19937 scalar_normal_dist_op<Scalar>::rng;
template<typename Scalar>
struct functor_traits<scalar_normal_dist_op<Scalar> >
{ enum { Cost = 50 * NumTraits<Scalar>::MulCost, PacketAccess = false, IsRepeatable = false }; };
} // end namespace internal
} // end namespace Eigen
/*
Draw nn samples from a size-dimensional normal distribution
with a specified mean and covariance
*/
void main()
{
int size = 2; // Dimensionality (rows)
int nn=5; // How many samples (columns) to draw
Eigen::internal::scalar_normal_dist_op<double> randN; // Gaussian functor
Eigen::internal::scalar_normal_dist_op<double>::rng.seed(1); // Seed the rng
// Define mean and covariance of the distribution
Eigen::VectorXd mean(size);
Eigen::MatrixXd covar(size,size);
mean << 0, 0;
covar << 1, .5,
.5, 1;
Eigen::MatrixXd normTransform(size,size);
Eigen::LLT<Eigen::MatrixXd> cholSolver(covar);
// We can only use the cholesky decomposition if
// the covariance matrix is symmetric, pos-definite.
// But a covariance matrix might be pos-semi-definite.
// In that case, we'll go to an EigenSolver
if (cholSolver.info()==Eigen::Success) {
// Use cholesky solver
normTransform = cholSolver.matrixL();
} else {
// Use eigen solver
Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> eigenSolver(covar);
normTransform = eigenSolver.eigenvectors()
* eigenSolver.eigenvalues().cwiseSqrt().asDiagonal();
}
Eigen::MatrixXd samples = (normTransform
* Eigen::MatrixXd::NullaryExpr(size,nn,randN)).colwise()
+ mean;
std::cout << "Mean\n" << mean << std::endl;
std::cout << "Covar\n" << covar << std::endl;
std::cout << "Samples\n" << samples << std::endl;
}
Voici une classe pour générer des variables aléatoires normales multivariées dans Eigen qui utilise la génération de nombres aléatoires C++11 et évite le Eigen::internal
trucs à l'aide de Eigen::MatrixBase::unaryExpr()
:
struct normal_random_variable
{
normal_random_variable(Eigen::MatrixXd const& covar)
: normal_random_variable(Eigen::VectorXd::Zero(covar.rows()), covar)
{}
normal_random_variable(Eigen::VectorXd const& mean, Eigen::MatrixXd const& covar)
: mean(mean)
{
Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> eigenSolver(covar);
transform = eigenSolver.eigenvectors() * eigenSolver.eigenvalues().cwiseSqrt().asDiagonal();
}
Eigen::VectorXd mean;
Eigen::MatrixXd transform;
Eigen::VectorXd operator()() const
{
static std::mt19937 gen{ std::random_device{}() };
static std::normal_distribution<> dist;
return mean + transform * Eigen::VectorXd{ mean.size() }.unaryExpr([&](auto x) { return dist(gen); });
}
};
Il peut être utilisé comme
int size = 2;
Eigen::MatrixXd covar(size,size);
covar << 1, .5,
.5, 1;
normal_random_variable sample { covar };
std::cout << sample() << std::endl;
std::cout << sample() << std::endl;
Qu'en est-il de faire un SVD et ensuite vérifier si la matrice est PD? Notez que cela ne vous oblige pas à calculer la factorisation Cholskey. Bien que, je pense SVD est plus lent que Cholskey, mais ils doivent tous les deux être cubique en nombre de flops.