Skip to content

Commit e23531f

Browse files
SiegeLordExSiegeLord
authored andcommitted
Add MultivariateNormalDiag distribution.
This is an extremely common special case of the MultivariateNormal distributon due to its efficient sampling and log-probability computations.
1 parent 5411ba7 commit e23531f

2 files changed

Lines changed: 347 additions & 0 deletions

File tree

src/distribution/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub use self::laplace::Laplace;
2626
pub use self::log_normal::LogNormal;
2727
pub use self::multinomial::Multinomial;
2828
pub use self::multivariate_normal::MultivariateNormal;
29+
pub use self::multivariate_normal_diag::MultivariateNormalDiag;
2930
pub use self::negative_binomial::NegativeBinomial;
3031
pub use self::normal::Normal;
3132
pub use self::pareto::Pareto;
@@ -59,6 +60,7 @@ mod laplace;
5960
mod log_normal;
6061
mod multinomial;
6162
mod multivariate_normal;
63+
mod multivariate_normal_diag;
6264
mod negative_binomial;
6365
mod normal;
6466
mod pareto;
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
use crate::distribution::Continuous;
2+
use crate::distribution::Normal;
3+
use crate::statistics::{Max, MeanN, Min, Mode, VarianceN};
4+
use crate::{consts, Result, StatsError};
5+
use nalgebra::DVector;
6+
use nalgebra::{
7+
base::allocator::Allocator, base::dimension::DimName, Cholesky, DefaultAllocator, Dim, DimMin,
8+
Matrix, LU, U1,
9+
};
10+
use rand::Rng;
11+
use std::f64;
12+
use std::f64::consts::{E, LN_2, PI};
13+
14+
/// Implements the [Multivariate Normal](https://en.wikipedia.org/wiki/Multivariate_normal_distribution)
15+
/// distribution with a diagonal covariance matrix using the "nalgebra" crate for vector
16+
/// operations. This specialization enables a considerably more efficient implementation than
17+
/// the full covariance matrix used in the MultivariateNormal distribution.
18+
///
19+
/// # Examples
20+
///
21+
/// ```
22+
/// use statrs::distribution::{MultivariateNormalDiag, Continuous};
23+
/// use nalgebra::DVector;
24+
/// use statrs::statistics::{MeanN, VarianceN};
25+
/// use statrs::assert_almost_eq;
26+
///
27+
/// let mvn = MultivariateNormalDiag::new(vec![0., 0.], vec![1., 1.]).unwrap();
28+
/// assert_eq!(mvn.mean().unwrap(), DVector::from_vec(vec![0., 0.]));
29+
/// assert_eq!(mvn.variance().unwrap(), DVector::from_vec(vec![1., 1.]));
30+
/// assert_almost_eq!(mvn.pdf(&DVector::from_vec(vec![1., 1.])), 1e-16, 0.05854983152431917);
31+
/// ```
32+
#[derive(Debug, Clone, PartialEq)]
33+
pub struct MultivariateNormalDiag {
34+
mu: DVector<f64>,
35+
std_dev: DVector<f64>,
36+
}
37+
38+
impl MultivariateNormalDiag {
39+
/// Constructs a new multivariate normal distribution with a mean of `mean`
40+
/// and covariance matrix with a diagonal of `std_dev * std_dev`
41+
///
42+
/// # Errors
43+
///
44+
/// Returns an error if `mean` or `std_dev` are `NaN` or if
45+
/// `std_dev <= 0.0`
46+
pub fn new(mean: Vec<f64>, std_dev: Vec<f64>) -> Result<Self> {
47+
let mean = DVector::from_vec(mean);
48+
let std_dev = DVector::from_vec(std_dev);
49+
// Check that all std_devs are positive
50+
if std_dev.iter().any(|&f| f <= 0.)
51+
// Check that mean and std_dev do not contain NaN
52+
|| mean.iter().any(|f| f.is_nan())
53+
|| std_dev.iter().any(|f| f.is_nan())
54+
// Check that the dimensions match
55+
|| mean.nrows() != std_dev.nrows()
56+
{
57+
return Err(StatsError::BadParams);
58+
}
59+
Ok(MultivariateNormalDiag { mu: mean, std_dev })
60+
}
61+
/// Returns the entropy of the multivariate normal distribution
62+
///
63+
/// # Formula
64+
///
65+
/// ```ignore
66+
/// (1 / 2) * ln(det(2 * π * e * Σ))
67+
/// ```
68+
///
69+
/// where `Σ` is the std_dev matrix and `det` is the determinant
70+
pub fn entropy(&self) -> Option<f64> {
71+
Some(self.std_dev.map(f64::ln).sum() + self.std_dev.nrows() as f64 * consts::LN_SQRT_2PIE)
72+
}
73+
}
74+
75+
impl ::rand::distributions::Distribution<DVector<f64>> for MultivariateNormalDiag {
76+
/// Samples from the multivariate normal distribution
77+
///
78+
/// # Formula
79+
/// std_dev * Z + μ
80+
///
81+
/// where `L` is the Cholesky decomposition of the covariance matrix,
82+
/// `Z` is a vector of normally distributed random variables, and
83+
/// `μ` is the mean vector
84+
85+
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> DVector<f64> {
86+
let d = Normal::new(0., 1.).unwrap();
87+
let z = DVector::<f64>::from_distribution(self.mu.nrows(), &d, rng);
88+
(&self.std_dev.component_mul(&z)) + &self.mu
89+
}
90+
}
91+
92+
impl Min<DVector<f64>> for MultivariateNormalDiag {
93+
/// Returns the minimum value in the domain of the
94+
/// multivariate normal distribution represented by a real vector
95+
fn min(&self) -> DVector<f64> {
96+
DVector::from_vec(vec![f64::NEG_INFINITY; self.mu.nrows()])
97+
}
98+
}
99+
100+
impl Max<DVector<f64>> for MultivariateNormalDiag {
101+
/// Returns the maximum value in the domain of the
102+
/// multivariate normal distribution represented by a real vector
103+
fn max(&self) -> DVector<f64> {
104+
DVector::from_vec(vec![f64::INFINITY; self.mu.nrows()])
105+
}
106+
}
107+
108+
impl MeanN<DVector<f64>> for MultivariateNormalDiag {
109+
/// Returns the mean of the normal distribution
110+
///
111+
/// # Remarks
112+
///
113+
/// This is the same mean used to construct the distribution
114+
fn mean(&self) -> Option<DVector<f64>> {
115+
let mut vec = vec![];
116+
for elt in self.mu.clone().into_iter() {
117+
vec.push(*elt);
118+
}
119+
Some(DVector::from_vec(vec))
120+
}
121+
}
122+
123+
impl VarianceN<DVector<f64>> for MultivariateNormalDiag {
124+
/// Returns the variance vector of the multivariate normal distribution
125+
fn variance(&self) -> Option<DVector<f64>> {
126+
Some(self.std_dev.component_mul(&self.std_dev))
127+
}
128+
}
129+
130+
impl Mode<DVector<f64>> for MultivariateNormalDiag {
131+
/// Returns the mode of the multivariate normal distribution
132+
///
133+
/// # Formula
134+
///
135+
/// ```ignore
136+
/// μ
137+
/// ```
138+
///
139+
/// where `μ` is the mean
140+
fn mode(&self) -> DVector<f64> {
141+
self.mu.clone()
142+
}
143+
}
144+
145+
impl<'a> Continuous<&'a DVector<f64>, f64> for MultivariateNormalDiag {
146+
/// Calculates the probability density function for the multivariate
147+
/// normal distribution at `x`
148+
///
149+
/// # Formula
150+
///
151+
/// ```ignore
152+
/// (2 * π) ^ (-k / 2) * det(Σ) ^ (1 / 2) * e ^ ( -(1 / 2) * transpose(x - μ) * inv(Σ) * (x - μ))
153+
/// ```
154+
///
155+
/// where `μ` is the mean, `inv(Σ)` is the precision matrix, `det(Σ)` is the determinant
156+
/// of the covariance matrix, and `k` is the dimension of the distribution
157+
fn pdf(&self, x: &'a DVector<f64>) -> f64 {
158+
let z = (x - &self.mu).component_div(&self.std_dev);
159+
// TODO: Use Matrix product from newer nalgebra.
160+
(-0.5 * z.component_mul(&z).sum()).exp()
161+
/ (&(&self.std_dev * consts::SQRT_2PI))
162+
.iter()
163+
.product::<f64>()
164+
}
165+
/// Calculates the log probability density function for the multivariate
166+
/// normal distribution at `x`. Equivalent to pdf(x).ln().
167+
fn ln_pdf(&self, x: &'a DVector<f64>) -> f64 {
168+
let z = (x - &self.mu).component_div(&self.std_dev);
169+
(-0.5 * z.component_mul(&z)).sum()
170+
- self
171+
.std_dev
172+
.map(f64::ln)
173+
.map(|x| x + consts::LN_SQRT_2PI)
174+
.sum()
175+
}
176+
}
177+
178+
#[rustfmt::skip]
179+
#[cfg(all(test, feature = "nightly"))]
180+
mod tests {
181+
use crate::distribution::{Continuous, MultivariateNormalDiag};
182+
use crate::statistics::*;
183+
use crate::consts::ACC;
184+
use core::fmt::Debug;
185+
use nalgebra::base::allocator::Allocator;
186+
use nalgebra::{
187+
DefaultAllocator, Dim, DimMin, DimName, DMatrix, Matrix2, Matrix3, Vector2, Vector3,
188+
U1, U2,
189+
};
190+
use rand::rngs::StdRng;
191+
use rand::distributions::Distribution;
192+
use rand::prelude::*;
193+
194+
fn try_create(mean: Vec<f64>, std_dev: Vec<f64>) -> MultivariateNormalDiag
195+
{
196+
let mvn = MultivariateNormalDiag::new(mean, std_dev);
197+
assert!(mvn.is_ok());
198+
mvn.unwrap()
199+
}
200+
201+
fn create_case(mean: Vec<f64>, std_dev: Vec<f64>)
202+
{
203+
let mvn = try_create(mean.clone(), std_dev.clone());
204+
assert_eq!(DVector::from_vec(mean.clone()), mvn.mean().unwrap());
205+
let std_dev = DVector::from_vec(std_dev);
206+
assert_eq!(std_dev.component_mul(&std_dev), mvn.variance().unwrap());
207+
}
208+
209+
fn bad_create_case(mean: Vec<f64>, std_dev: Vec<f64>)
210+
{
211+
let mvn = MultivariateNormalDiag::new(mean, std_dev);
212+
assert!(mvn.is_err());
213+
}
214+
215+
fn test_case<T, F>(mean: Vec<f64>, std_dev: Vec<f64>, expected: T, eval: F)
216+
where
217+
T: Debug + PartialEq,
218+
F: FnOnce(MultivariateNormalDiag) -> T,
219+
{
220+
let mvn = try_create(mean, std_dev);
221+
let x = eval(mvn);
222+
assert_eq!(expected, x);
223+
}
224+
225+
fn test_almost<F>(
226+
mean: Vec<f64>,
227+
std_dev: Vec<f64>,
228+
expected: f64,
229+
acc: f64,
230+
eval: F,
231+
) where
232+
F: FnOnce(MultivariateNormalDiag) -> f64,
233+
{
234+
let mvn = try_create(mean, std_dev);
235+
let x = eval(mvn);
236+
assert_almost_eq!(expected, x, acc);
237+
}
238+
239+
use super::*;
240+
241+
macro_rules! dvec {
242+
($($x:expr),*) => (DVector::from_vec(vec![$($x),*]));
243+
}
244+
245+
#[test]
246+
fn test_create() {
247+
create_case(vec![0., 0.], vec![1., 1.]);
248+
create_case(vec![10., 5.], vec![2., 2.]);
249+
create_case(vec![4., 5., 6.], vec![2., 2., 2.]);
250+
create_case(vec![0., f64::INFINITY], vec![1., 1.]);
251+
create_case(vec![0., 0.], vec![f64::INFINITY, f64::INFINITY]);
252+
}
253+
254+
#[test]
255+
fn test_bad_create() {
256+
// std_dev not positive
257+
bad_create_case(vec![0., 0.], vec![0., 1.]);
258+
// NaN in mean
259+
bad_create_case(vec![0., f64::NAN], vec![1., 1.]);
260+
// NaN in std_dev
261+
bad_create_case(vec![0., 0.], vec![1., f64::NAN]);
262+
}
263+
264+
#[test]
265+
fn test_variance() {
266+
let variance = |x: MultivariateNormalDiag| x.variance().unwrap();
267+
test_case(vec![0., 0.], vec![1., 1.], dvec![1., 1.], variance);
268+
test_case(vec![0., 0.], vec![2., 2.], dvec![4., 4.], variance);
269+
test_case(vec![0., 0.], vec![f64::INFINITY, f64::INFINITY], dvec![f64::INFINITY, f64::INFINITY], variance);
270+
}
271+
272+
#[test]
273+
fn test_entropy() {
274+
let entropy = |x: MultivariateNormalDiag| x.entropy().unwrap();
275+
test_case(vec![0., 0.], vec![1., 1.], 2.8378770664093453, entropy);
276+
test_case(vec![0., 0.], vec![f64::INFINITY, f64::INFINITY], f64::INFINITY, entropy);
277+
}
278+
279+
#[test]
280+
fn test_mode() {
281+
let mode = |x: MultivariateNormalDiag| x.mode();
282+
test_case(vec![0., 0.], vec![1., 1.], dvec![0., 0.], mode);
283+
test_case(vec![f64::INFINITY, f64::INFINITY], vec![1., 1.], dvec![f64::INFINITY, f64::INFINITY], mode);
284+
}
285+
286+
#[test]
287+
fn test_min_max() {
288+
let min = |x: MultivariateNormalDiag| x.min();
289+
let max = |x: MultivariateNormalDiag| x.max();
290+
test_case(vec![0., 0.], vec![1., 1.], dvec![f64::NEG_INFINITY, f64::NEG_INFINITY], min);
291+
test_case(vec![0., 0.], vec![1., 1.], dvec![f64::INFINITY, f64::INFINITY], max);
292+
test_case(vec![10., 1.], vec![1., 1.], dvec![f64::NEG_INFINITY, f64::NEG_INFINITY], min);
293+
test_case(vec![-3., 5.], vec![1., 1.], dvec![f64::INFINITY, f64::INFINITY], max);
294+
}
295+
296+
#[test]
297+
fn test_pdf() {
298+
let pdf = |arg: DVector<f64>| move |x: MultivariateNormalDiag| x.pdf(&arg);
299+
test_almost(vec![0., 0.], vec![1., 1.], 0.05854983152431917, 1e-15, pdf(dvec![1., 1.]));
300+
test_almost(vec![0., 0.], vec![1., 1.], 0.013064233284684921, 1e-15, pdf(dvec![1., 2.]));
301+
test_almost(vec![1., 2.], vec![3., 4.], 0.013262911924324607, 1e-15, pdf(dvec![1., 2.]));
302+
test_almost(vec![0., 0.], vec![1., 1.], 1.8618676045881531e-23, 1e-35, pdf(dvec![1., 10.]));
303+
test_almost(vec![0., 0.], vec![1., 1.], 5.920684802611216e-45, 1e-58, pdf(dvec![10., 10.]));
304+
test_almost(vec![1., 1.], vec![1., 1.], 5.920684802611216e-45, 1e-58, pdf(dvec![11., 11.]));
305+
test_case(vec![0., 0.], vec![f64::INFINITY, f64::INFINITY], 0.0, pdf(dvec![10., 10.]));
306+
test_case(vec![0., 0.], vec![f64::INFINITY, f64::INFINITY], 0.0, pdf(dvec![100., 100.]));
307+
}
308+
309+
#[test]
310+
fn test_ln_pdf() {
311+
let ln_pdf = |arg: DVector<_>| move |x: MultivariateNormalDiag| x.ln_pdf(&arg);
312+
test_almost(vec![0., 0.], vec![1., 1.], (0.05854983152431917f64).ln(), 1e-15, ln_pdf(dvec![1., 1.]));
313+
test_almost(vec![0., 0.], vec![1., 1.], (0.013064233284684921f64).ln(), 1e-15, ln_pdf(dvec![1., 2.]));
314+
test_almost(vec![1., 2.], vec![3., 4.], (0.013262911924324607f64).ln(), 1e-15, ln_pdf(dvec![1., 2.]));
315+
test_almost(vec![0., 0.], vec![1., 1.], (1.8618676045881531e-23f64).ln(), 1e-15, ln_pdf(dvec![1., 10.]));
316+
test_almost(vec![0., 0.], vec![1., 1.], (5.920684802611216e-45f64).ln(), 1e-15, ln_pdf(dvec![10., 10.]));
317+
test_case(vec![0., 0.], vec![f64::INFINITY, f64::INFINITY], f64::NEG_INFINITY, ln_pdf(dvec![10., 10.]));
318+
test_case(vec![0., 0.], vec![f64::INFINITY, f64::INFINITY], f64::NEG_INFINITY, ln_pdf(dvec![100., 100.]));
319+
}
320+
321+
#[test]
322+
fn test_sample() {
323+
const N: usize = 10000;
324+
let mean = dvec![1., 2.];
325+
let std_dev = dvec![3., 4.];
326+
let mvn = try_create(mean.iter().copied().collect(), std_dev.iter().copied().collect());
327+
let mut rng = StdRng::seed_from_u64(0);
328+
let mut samples = DMatrix::zeros(N, mean.nrows());
329+
for i in 0..N
330+
{
331+
samples.set_row(i, &mvn.sample(&mut rng).transpose());
332+
}
333+
334+
for (i, &mean) in mean.iter().enumerate()
335+
{
336+
let est_mean = samples.column(i).mean();
337+
assert_almost_eq!(mean, est_mean, 0.1);
338+
}
339+
for (i, &std_dev) in std_dev.iter().enumerate()
340+
{
341+
let est_std_dev = samples.column(i).std_dev();
342+
assert_almost_eq!(std_dev, est_std_dev, 0.1);
343+
}
344+
}
345+
}

0 commit comments

Comments
 (0)