gonum.org/v1/gonum@v0.15.1-0.20240517103525-f853624cb1bb/stat/distmv/normal.go (about) 1 // Copyright ©2015 The Gonum Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package distmv 6 7 import ( 8 "math" 9 10 "golang.org/x/exp/rand" 11 12 "gonum.org/v1/gonum/floats" 13 "gonum.org/v1/gonum/mat" 14 "gonum.org/v1/gonum/stat" 15 "gonum.org/v1/gonum/stat/distuv" 16 ) 17 18 // Normal is a multivariate normal distribution (also known as the multivariate 19 // Gaussian distribution). Its pdf in k dimensions is given by 20 // 21 // (2 π)^(-k/2) |Σ|^(-1/2) exp(-1/2 (x-μ)'Σ^-1(x-μ)) 22 // 23 // where μ is the mean vector and Σ the covariance matrix. Σ must be symmetric 24 // and positive definite. Use NewNormal to construct. 25 type Normal struct { 26 mu []float64 27 28 sigma mat.SymDense 29 30 chol mat.Cholesky 31 logSqrtDet float64 32 dim int 33 34 // If src is altered, rnd must be updated. 35 src rand.Source 36 rnd *rand.Rand 37 } 38 39 // NewNormal creates a new Normal with the given mean and covariance matrix. 40 // NewNormal panics if len(mu) == 0, or if len(mu) != sigma.N. If the covariance 41 // matrix is not positive-definite, the returned boolean is false. 42 func NewNormal(mu []float64, sigma mat.Symmetric, src rand.Source) (*Normal, bool) { 43 if len(mu) == 0 { 44 panic(badZeroDimension) 45 } 46 dim := sigma.SymmetricDim() 47 if dim != len(mu) { 48 panic(badSizeMismatch) 49 } 50 n := &Normal{ 51 src: src, 52 rnd: rand.New(src), 53 dim: dim, 54 mu: make([]float64, dim), 55 } 56 copy(n.mu, mu) 57 ok := n.chol.Factorize(sigma) 58 if !ok { 59 return nil, false 60 } 61 n.sigma = *mat.NewSymDense(dim, nil) 62 n.sigma.CopySym(sigma) 63 n.logSqrtDet = 0.5 * n.chol.LogDet() 64 return n, true 65 } 66 67 // NewNormalChol creates a new Normal distribution with the given mean and 68 // covariance matrix represented by its Cholesky decomposition. NewNormalChol 69 // panics if len(mu) is not equal to chol.Size(). 70 func NewNormalChol(mu []float64, chol *mat.Cholesky, src rand.Source) *Normal { 71 dim := len(mu) 72 if dim != chol.SymmetricDim() { 73 panic(badSizeMismatch) 74 } 75 n := &Normal{ 76 src: src, 77 rnd: rand.New(src), 78 dim: dim, 79 mu: make([]float64, dim), 80 } 81 n.chol.Clone(chol) 82 copy(n.mu, mu) 83 n.logSqrtDet = 0.5 * n.chol.LogDet() 84 return n 85 } 86 87 // NewNormalPrecision creates a new Normal distribution with the given mean and 88 // precision matrix (inverse of the covariance matrix). NewNormalPrecision 89 // panics if len(mu) is not equal to prec.SymmetricDim(). If the precision matrix 90 // is not positive-definite, NewNormalPrecision returns nil for norm and false 91 // for ok. 92 func NewNormalPrecision(mu []float64, prec *mat.SymDense, src rand.Source) (norm *Normal, ok bool) { 93 if len(mu) == 0 { 94 panic(badZeroDimension) 95 } 96 dim := prec.SymmetricDim() 97 if dim != len(mu) { 98 panic(badSizeMismatch) 99 } 100 // TODO(btracey): Computing a matrix inverse is generally numerically unstable. 101 // This only has to compute the inverse of a positive definite matrix, which 102 // is much better, but this still loses precision. It is worth considering if 103 // instead the precision matrix should be stored explicitly and used instead 104 // of the Cholesky decomposition of the covariance matrix where appropriate. 105 var chol mat.Cholesky 106 ok = chol.Factorize(prec) 107 if !ok { 108 return nil, false 109 } 110 var sigma mat.SymDense 111 err := chol.InverseTo(&sigma) 112 if err != nil { 113 return nil, false 114 } 115 return NewNormal(mu, &sigma, src) 116 } 117 118 // ConditionNormal returns the Normal distribution that is the receiver conditioned 119 // on the input evidence. The returned multivariate normal has dimension 120 // n - len(observed), where n is the dimension of the original receiver. The updated 121 // mean and covariance are 122 // 123 // mu = mu_un + sigma_{ob,un}ᵀ * sigma_{ob,ob}^-1 (v - mu_ob) 124 // sigma = sigma_{un,un} - sigma_{ob,un}ᵀ * sigma_{ob,ob}^-1 * sigma_{ob,un} 125 // 126 // where mu_un and mu_ob are the original means of the unobserved and observed 127 // variables respectively, sigma_{un,un} is the unobserved subset of the covariance 128 // matrix, sigma_{ob,ob} is the observed subset of the covariance matrix, and 129 // sigma_{un,ob} are the cross terms. The elements of x_2 have been observed with 130 // values v. The dimension order is preserved during conditioning, so if the value 131 // of dimension 1 is observed, the returned normal represents dimensions {0, 2, ...} 132 // of the original Normal distribution. 133 // 134 // ConditionNormal returns {nil, false} if there is a failure during the update. 135 // Mathematically this is impossible, but can occur with finite precision arithmetic. 136 func (n *Normal) ConditionNormal(observed []int, values []float64, src rand.Source) (*Normal, bool) { 137 if len(observed) == 0 { 138 panic("normal: no observed value") 139 } 140 if len(observed) != len(values) { 141 panic(badInputLength) 142 } 143 for _, v := range observed { 144 if v < 0 || v >= n.Dim() { 145 panic("normal: observed value out of bounds") 146 } 147 } 148 149 _, mu1, sigma11 := studentsTConditional(observed, values, math.Inf(1), n.mu, &n.sigma) 150 if mu1 == nil { 151 return nil, false 152 } 153 return NewNormal(mu1, sigma11, src) 154 } 155 156 // CovarianceMatrix stores the covariance matrix of the distribution in dst. 157 // Upon return, the value at element {i, j} of the covariance matrix is equal 158 // to the covariance of the i^th and j^th variables. 159 // 160 // covariance(i, j) = E[(x_i - E[x_i])(x_j - E[x_j])] 161 // 162 // If the dst matrix is empty it will be resized to the correct dimensions, 163 // otherwise dst must match the dimension of the receiver or CovarianceMatrix 164 // will panic. 165 func (n *Normal) CovarianceMatrix(dst *mat.SymDense) { 166 if dst.IsEmpty() { 167 *dst = *(dst.GrowSym(n.dim).(*mat.SymDense)) 168 } else if dst.SymmetricDim() != n.dim { 169 panic("normal: input matrix size mismatch") 170 } 171 dst.CopySym(&n.sigma) 172 } 173 174 // Dim returns the dimension of the distribution. 175 func (n *Normal) Dim() int { 176 return n.dim 177 } 178 179 // Entropy returns the differential entropy of the distribution. 180 func (n *Normal) Entropy() float64 { 181 return float64(n.dim)/2*(1+logTwoPi) + n.logSqrtDet 182 } 183 184 // LogProb computes the log of the pdf of the point x. 185 func (n *Normal) LogProb(x []float64) float64 { 186 dim := n.dim 187 if len(x) != dim { 188 panic(badSizeMismatch) 189 } 190 return normalLogProb(x, n.mu, &n.chol, n.logSqrtDet) 191 } 192 193 // NormalLogProb computes the log probability of the location x for a Normal 194 // distribution the given mean and Cholesky decomposition of the covariance matrix. 195 // NormalLogProb panics if len(x) is not equal to len(mu), or if len(mu) != chol.Size(). 196 // 197 // This function saves time and memory if the Cholesky decomposition is already 198 // available. Otherwise, the NewNormal function should be used. 199 func NormalLogProb(x, mu []float64, chol *mat.Cholesky) float64 { 200 dim := len(mu) 201 if len(x) != dim { 202 panic(badSizeMismatch) 203 } 204 if chol.SymmetricDim() != dim { 205 panic(badSizeMismatch) 206 } 207 logSqrtDet := 0.5 * chol.LogDet() 208 return normalLogProb(x, mu, chol, logSqrtDet) 209 } 210 211 // normalLogProb is the same as NormalLogProb, but does not make size checks and 212 // additionally requires log(|Σ|^-0.5) 213 func normalLogProb(x, mu []float64, chol *mat.Cholesky, logSqrtDet float64) float64 { 214 dim := len(mu) 215 c := -0.5*float64(dim)*logTwoPi - logSqrtDet 216 dst := stat.Mahalanobis(mat.NewVecDense(dim, x), mat.NewVecDense(dim, mu), chol) 217 return c - 0.5*dst*dst 218 } 219 220 // MarginalNormal returns the marginal distribution of the given input variables. 221 // That is, MarginalNormal returns 222 // 223 // p(x_i) = \int_{x_o} p(x_i | x_o) p(x_o) dx_o 224 // 225 // where x_i are the dimensions in the input, and x_o are the remaining dimensions. 226 // See https://en.wikipedia.org/wiki/Marginal_distribution for more information. 227 // 228 // The input src is passed to the call to NewNormal. 229 func (n *Normal) MarginalNormal(vars []int, src rand.Source) (*Normal, bool) { 230 newMean := make([]float64, len(vars)) 231 for i, v := range vars { 232 newMean[i] = n.mu[v] 233 } 234 var s mat.SymDense 235 s.SubsetSym(&n.sigma, vars) 236 return NewNormal(newMean, &s, src) 237 } 238 239 // MarginalNormalSingle returns the marginal of the given input variable. 240 // That is, MarginalNormal returns 241 // 242 // p(x_i) = \int_{x_¬i} p(x_i | x_¬i) p(x_¬i) dx_¬i 243 // 244 // where i is the input index. 245 // See https://en.wikipedia.org/wiki/Marginal_distribution for more information. 246 // 247 // The input src is passed to the constructed distuv.Normal. 248 func (n *Normal) MarginalNormalSingle(i int, src rand.Source) distuv.Normal { 249 return distuv.Normal{ 250 Mu: n.mu[i], 251 Sigma: math.Sqrt(n.sigma.At(i, i)), 252 Src: src, 253 } 254 } 255 256 // Mean returns the mean of the probability distribution. 257 // 258 // If dst is not nil, the mean will be stored in-place into dst and returned, 259 // otherwise a new slice will be allocated first. If dst is not nil, it must 260 // have length equal to the dimension of the distribution. 261 func (n *Normal) Mean(dst []float64) []float64 { 262 dst = reuseAs(dst, n.dim) 263 copy(dst, n.mu) 264 return dst 265 } 266 267 // Prob computes the value of the probability density function at x. 268 func (n *Normal) Prob(x []float64) float64 { 269 return math.Exp(n.LogProb(x)) 270 } 271 272 // Quantile returns the value of the multi-dimensional inverse cumulative 273 // distribution function at p. 274 // 275 // If dst is not nil, the quantile will be stored in-place into dst and 276 // returned, otherwise a new slice will be allocated first. If dst is not nil, 277 // it must have length equal to the dimension of the distribution. Quantile will 278 // also panic if the length of p is not equal to the dimension of the 279 // distribution. 280 // 281 // All of the values of p must be between 0 and 1, inclusive, or Quantile will 282 // panic. 283 func (n *Normal) Quantile(dst, p []float64) []float64 { 284 if len(p) != n.dim { 285 panic(badInputLength) 286 } 287 dst = reuseAs(dst, n.dim) 288 289 // Transform to a standard normal and then transform to a multivariate Gaussian. 290 for i, v := range p { 291 dst[i] = distuv.UnitNormal.Quantile(v) 292 } 293 n.TransformNormal(dst, dst) 294 return dst 295 } 296 297 // Rand generates a random sample according to the distributon. 298 // 299 // If dst is not nil, the sample will be stored in-place into dst and returned, 300 // otherwise a new slice will be allocated first. If dst is not nil, it must 301 // have length equal to the dimension of the distribution. 302 func (n *Normal) Rand(dst []float64) []float64 { 303 return NormalRand(dst, n.mu, &n.chol, n.src) 304 } 305 306 // NormalRand generates a random sample from a multivariate normal distributon 307 // given by the mean and the Cholesky factorization of the covariance matrix. 308 // 309 // If dst is not nil, the sample will be stored in-place into dst and returned, 310 // otherwise a new slice will be allocated first. If dst is not nil, it must 311 // have length equal to the dimension of the distribution. 312 // 313 // This function saves time and memory if the Cholesky factorization is already 314 // available. Otherwise, the NewNormal function should be used. 315 func NormalRand(dst, mean []float64, chol *mat.Cholesky, src rand.Source) []float64 { 316 if len(mean) != chol.SymmetricDim() { 317 panic(badInputLength) 318 } 319 dst = reuseAs(dst, len(mean)) 320 321 if src == nil { 322 for i := range dst { 323 dst[i] = rand.NormFloat64() 324 } 325 } else { 326 rnd := rand.New(src) 327 for i := range dst { 328 dst[i] = rnd.NormFloat64() 329 } 330 } 331 transformNormal(dst, dst, mean, chol) 332 return dst 333 } 334 335 // EigenSym is an eigendecomposition of a symmetric matrix. 336 type EigenSym interface { 337 mat.Symmetric 338 // RawValues returns all eigenvalues in ascending order. The returned slice 339 // must not be modified. 340 RawValues() []float64 341 // RawQ returns an orthogonal matrix whose columns contain the eigenvectors. 342 // The returned matrix must not be modified. 343 RawQ() mat.Matrix 344 } 345 346 // PositivePartEigenSym is an EigenSym that sets any negative eigenvalues from 347 // the given eigendecomposition to zero but otherwise returns the values 348 // unchanged. 349 // 350 // This is useful for filtering eigenvalues of positive semi-definite matrices 351 // that are almost zero but negative due to rounding errors. 352 type PositivePartEigenSym struct { 353 ed *mat.EigenSym 354 vals []float64 355 } 356 357 var _ EigenSym = (*PositivePartEigenSym)(nil) 358 var _ EigenSym = (*mat.EigenSym)(nil) 359 360 // NewPositivePartEigenSym returns a new PositivePartEigenSym, wrapping the 361 // given eigendecomposition. 362 func NewPositivePartEigenSym(ed *mat.EigenSym) *PositivePartEigenSym { 363 n := ed.SymmetricDim() 364 vals := make([]float64, n) 365 for i, lamda := range ed.RawValues() { 366 if lamda > 0 { 367 vals[i] = lamda 368 } 369 } 370 return &PositivePartEigenSym{ 371 ed: ed, 372 vals: vals, 373 } 374 } 375 376 // SymmetricDim returns the value from the wrapped eigendecomposition. 377 func (ed *PositivePartEigenSym) SymmetricDim() int { return ed.ed.SymmetricDim() } 378 379 // Dims returns the dimensions from the wrapped eigendecomposition. 380 func (ed *PositivePartEigenSym) Dims() (r, c int) { return ed.ed.Dims() } 381 382 // At returns the value from the wrapped eigendecomposition. 383 func (ed *PositivePartEigenSym) At(i, j int) float64 { return ed.ed.At(i, j) } 384 385 // T returns the transpose from the wrapped eigendecomposition. 386 func (ed *PositivePartEigenSym) T() mat.Matrix { return ed.ed.T() } 387 388 // RawQ returns the orthogonal matrix Q from the wrapped eigendecomposition. The 389 // returned matrix must not be modified. 390 func (ed *PositivePartEigenSym) RawQ() mat.Matrix { return ed.ed.RawQ() } 391 392 // RawValues returns the eigenvalues from the wrapped eigendecomposition in 393 // ascending order with any negative value replaced by zero. The returned slice 394 // must not be modified. 395 func (ed *PositivePartEigenSym) RawValues() []float64 { return ed.vals } 396 397 // NormalRandCov generates a random sample from a multivariate normal 398 // distribution given by the mean and the covariance matrix. 399 // 400 // If dst is not nil, the sample will be stored in-place into dst and returned, 401 // otherwise a new slice will be allocated first. If dst is not nil, it must 402 // have length equal to the dimension of the distribution. 403 // 404 // cov should be *mat.Cholesky, *mat.PivotedCholesky or EigenSym, otherwise 405 // NormalRandCov will be very inefficient because a pivoted Cholesky 406 // factorization of cov will be computed for every sample. 407 // 408 // If cov is an EigenSym, all eigenvalues returned by RawValues must be 409 // non-negative, otherwise NormalRandCov will panic. 410 func NormalRandCov(dst, mean []float64, cov mat.Symmetric, src rand.Source) []float64 { 411 n := len(mean) 412 if cov.SymmetricDim() != n { 413 panic(badInputLength) 414 } 415 dst = reuseAs(dst, n) 416 if src == nil { 417 for i := range dst { 418 dst[i] = rand.NormFloat64() 419 } 420 } else { 421 rnd := rand.New(src) 422 for i := range dst { 423 dst[i] = rnd.NormFloat64() 424 } 425 } 426 427 switch cov := cov.(type) { 428 case *mat.Cholesky: 429 dstVec := mat.NewVecDense(n, dst) 430 dstVec.MulVec(cov.RawU().T(), dstVec) 431 case *mat.PivotedCholesky: 432 dstVec := mat.NewVecDense(n, dst) 433 dstVec.MulVec(cov.RawU().T(), dstVec) 434 dstVec.Permute(cov.ColumnPivots(nil), true) 435 case EigenSym: 436 vals := cov.RawValues() 437 if vals[0] < 0 { 438 panic("distmv: covariance matrix is not positive semi-definite") 439 } 440 for i, val := range vals { 441 dst[i] *= math.Sqrt(val) 442 } 443 dstVec := mat.NewVecDense(n, dst) 444 dstVec.MulVec(cov.RawQ(), dstVec) 445 default: 446 var chol mat.PivotedCholesky 447 chol.Factorize(cov, -1) 448 dstVec := mat.NewVecDense(n, dst) 449 dstVec.MulVec(chol.RawU().T(), dstVec) 450 dstVec.Permute(chol.ColumnPivots(nil), true) 451 } 452 floats.Add(dst, mean) 453 454 return dst 455 } 456 457 // ScoreInput returns the gradient of the log-probability with respect to the 458 // input x. That is, ScoreInput computes 459 // 460 // ∇_x log(p(x)) 461 // 462 // If dst is not nil, the score will be stored in-place into dst and returned, 463 // otherwise a new slice will be allocated first. If dst is not nil, it must 464 // have length equal to the dimension of the distribution. 465 func (n *Normal) ScoreInput(dst, x []float64) []float64 { 466 // Normal log probability is 467 // c - 0.5*(x-μ)' Σ^-1 (x-μ). 468 // So the derivative is just 469 // -Σ^-1 (x-μ). 470 if len(x) != n.Dim() { 471 panic(badInputLength) 472 } 473 dst = reuseAs(dst, n.dim) 474 475 floats.SubTo(dst, x, n.mu) 476 dstVec := mat.NewVecDense(len(dst), dst) 477 err := n.chol.SolveVecTo(dstVec, dstVec) 478 if err != nil { 479 panic(err) 480 } 481 floats.Scale(-1, dst) 482 return dst 483 } 484 485 // SetMean changes the mean of the normal distribution. SetMean panics if len(mu) 486 // does not equal the dimension of the normal distribution. 487 func (n *Normal) SetMean(mu []float64) { 488 if len(mu) != n.Dim() { 489 panic(badSizeMismatch) 490 } 491 copy(n.mu, mu) 492 } 493 494 // TransformNormal transforms x generated from a standard multivariate normal 495 // into a vector that has been generated under the normal distribution of the 496 // receiver. 497 // 498 // If dst is not nil, the result will be stored in-place into dst and returned, 499 // otherwise a new slice will be allocated first. If dst is not nil, it must 500 // have length equal to the dimension of the distribution. TransformNormal will 501 // also panic if the length of x is not equal to the dimension of the receiver. 502 func (n *Normal) TransformNormal(dst, x []float64) []float64 { 503 if len(x) != n.dim { 504 panic(badInputLength) 505 } 506 dst = reuseAs(dst, n.dim) 507 transformNormal(dst, x, n.mu, &n.chol) 508 return dst 509 } 510 511 // transformNormal performs the same operation as Normal.TransformNormal except 512 // no safety checks are performed and all memory must be provided. 513 func transformNormal(dst, normal, mu []float64, chol *mat.Cholesky) []float64 { 514 dim := len(mu) 515 dstVec := mat.NewVecDense(dim, dst) 516 srcVec := mat.NewVecDense(dim, normal) 517 // If dst and normal are the same slice, make them the same Vector otherwise 518 // mat complains about being tricky. 519 if &normal[0] == &dst[0] { 520 srcVec = dstVec 521 } 522 dstVec.MulVec(chol.RawU().T(), srcVec) 523 floats.Add(dst, mu) 524 return dst 525 }