mirror of
https://github.com/gonum/gonum.git
synced 2025-10-07 16:11:03 +08:00
211 lines
5.5 KiB
Go
211 lines
5.5 KiB
Go
// Copyright ©2016 The Gonum Authors. All rights reserved.
|
||
// Use of this source code is governed by a BSD-style
|
||
// license that can be found in the LICENSE file.
|
||
|
||
package distmat
|
||
|
||
import (
|
||
"math"
|
||
"sync"
|
||
|
||
"golang.org/x/exp/rand"
|
||
|
||
"gonum.org/v1/gonum/mat"
|
||
"gonum.org/v1/gonum/mathext"
|
||
"gonum.org/v1/gonum/stat/distuv"
|
||
)
|
||
|
||
// Wishart is a distribution over d×d positive symmetric definite matrices. It
|
||
// is parametrized by a scalar degrees of freedom parameter ν and a d×d positive
|
||
// definite matrix V.
|
||
//
|
||
// The Wishart PDF is given by
|
||
// p(X) = [|X|^((ν-d-1)/2) * exp(-tr(V^-1 * X)/2)] / [2^(ν*d/2) * |V|^(ν/2) * Γ_d(ν/2)]
|
||
// where X is a d×d PSD matrix, ν > d-1, |·| denotes the determinant, tr is the
|
||
// trace and Γ_d is the multivariate gamma function.
|
||
//
|
||
// See https://en.wikipedia.org/wiki/Wishart_distribution for more information.
|
||
type Wishart struct {
|
||
nu float64
|
||
src rand.Source
|
||
|
||
dim int
|
||
cholv mat.Cholesky
|
||
logdetv float64
|
||
upper mat.TriDense
|
||
|
||
once sync.Once
|
||
v *mat.SymDense // only stored if needed
|
||
}
|
||
|
||
// NewWishart returns a new Wishart distribution with the given shape matrix and
|
||
// degrees of freedom parameter. NewWishart returns whether the creation was
|
||
// successful.
|
||
//
|
||
// NewWishart panics if nu <= d - 1 where d is the order of v.
|
||
func NewWishart(v mat.Symmetric, nu float64, src rand.Source) (*Wishart, bool) {
|
||
dim := v.Symmetric()
|
||
if nu <= float64(dim-1) {
|
||
panic("wishart: nu must be greater than dim-1")
|
||
}
|
||
var chol mat.Cholesky
|
||
ok := chol.Factorize(v)
|
||
if !ok {
|
||
return nil, false
|
||
}
|
||
|
||
var u mat.TriDense
|
||
chol.UTo(&u)
|
||
|
||
w := &Wishart{
|
||
nu: nu,
|
||
src: src,
|
||
|
||
dim: dim,
|
||
cholv: chol,
|
||
logdetv: chol.LogDet(),
|
||
upper: u,
|
||
}
|
||
return w, true
|
||
}
|
||
|
||
// MeanSym returns the mean matrix of the distribution as a symmetric matrix.
|
||
// If x is nil, a new matrix is allocated and returned. If x is not nil, the
|
||
// result is stored in-place into x and MeanSym will panic if the order of x
|
||
// is not equal to the order of the receiver.
|
||
func (w *Wishart) MeanSym(x *mat.SymDense) *mat.SymDense {
|
||
if x == nil {
|
||
x = mat.NewSymDense(w.dim, nil)
|
||
}
|
||
d := x.Symmetric()
|
||
if d != w.dim {
|
||
panic(badDim)
|
||
}
|
||
w.setV()
|
||
x.CopySym(w.v)
|
||
x.ScaleSym(w.nu, x)
|
||
return x
|
||
}
|
||
|
||
// ProbSym returns the probability of the symmetric matrix x. If x is not positive
|
||
// definite (the Cholesky decomposition fails), it has 0 probability.
|
||
func (w *Wishart) ProbSym(x mat.Symmetric) float64 {
|
||
return math.Exp(w.LogProbSym(x))
|
||
}
|
||
|
||
// LogProbSym returns the log of the probability of the input symmetric matrix.
|
||
//
|
||
// LogProbSym returns -∞ if the input matrix is not positive definite (the Cholesky
|
||
// decomposition fails).
|
||
func (w *Wishart) LogProbSym(x mat.Symmetric) float64 {
|
||
dim := x.Symmetric()
|
||
if dim != w.dim {
|
||
panic(badDim)
|
||
}
|
||
var chol mat.Cholesky
|
||
ok := chol.Factorize(x)
|
||
if !ok {
|
||
return math.Inf(-1)
|
||
}
|
||
return w.logProbSymChol(&chol)
|
||
}
|
||
|
||
// LogProbSymChol returns the log of the probability of the input symmetric matrix
|
||
// given its Cholesky decomposition.
|
||
func (w *Wishart) LogProbSymChol(cholX *mat.Cholesky) float64 {
|
||
dim := cholX.Size()
|
||
if dim != w.dim {
|
||
panic(badDim)
|
||
}
|
||
return w.logProbSymChol(cholX)
|
||
}
|
||
|
||
func (w *Wishart) logProbSymChol(cholX *mat.Cholesky) float64 {
|
||
// The PDF is
|
||
// p(X) = [|X|^((ν-d-1)/2) * exp(-tr(V^-1 * X)/2)] / [2^(ν*d/2) * |V|^(ν/2) * Γ_d(ν/2)]
|
||
// The LogPDF is thus
|
||
// (ν-d-1)/2 * log(|X|) - tr(V^-1 * X)/2 - (ν*d/2)*log(2) - ν/2 * log(|V|) - log(Γ_d(ν/2))
|
||
logdetx := cholX.LogDet()
|
||
|
||
// Compute tr(V^-1 * X), using the fact that X = U^T * U.
|
||
var u mat.TriDense
|
||
cholX.UTo(&u)
|
||
|
||
var vinvx mat.Dense
|
||
err := w.cholv.Solve(&vinvx, u.T())
|
||
if err != nil {
|
||
return math.Inf(-1)
|
||
}
|
||
vinvx.Mul(&vinvx, &u)
|
||
tr := mat.Trace(&vinvx)
|
||
|
||
fnu := float64(w.nu)
|
||
fdim := float64(w.dim)
|
||
|
||
return 0.5*((fnu-fdim-1)*logdetx-tr-fnu*fdim*math.Ln2-fnu*w.logdetv) - mathext.MvLgamma(0.5*fnu, w.dim)
|
||
}
|
||
|
||
// RandSym generates a random symmetric matrix from the distribution.
|
||
func (w *Wishart) RandSym(x *mat.SymDense) *mat.SymDense {
|
||
if x == nil {
|
||
x = &mat.SymDense{}
|
||
}
|
||
var c mat.Cholesky
|
||
w.RandChol(&c)
|
||
c.ToSym(x)
|
||
return x
|
||
}
|
||
|
||
// RandChol generates the Cholesky decomposition of a random matrix from the distribution.
|
||
func (w *Wishart) RandChol(c *mat.Cholesky) *mat.Cholesky {
|
||
// TODO(btracey): Modify the code if the underlying data from c is exposed
|
||
// to avoid the dim^2 allocation here.
|
||
|
||
// Use the Bartlett Decomposition, which says that
|
||
// X ~ L A A^T L^T
|
||
// Where A is a lower triangular matrix in which the diagonal of A is
|
||
// generated from the square roots of χ^2 random variables, and the
|
||
// off-diagonals are generated from standard normal variables.
|
||
// The above gives the cholesky decomposition of X, where L_x = L A.
|
||
//
|
||
// mat64 works with the upper triagular decomposition, so we would like to do
|
||
// the same. We can instead say that
|
||
// U_x = L_x^T = (L * A)^T = A^T * L^T = A^T * U
|
||
// Instead, generate A^T, by using the procedure above, except as an upper
|
||
// triangular matrix.
|
||
norm := distuv.Normal{
|
||
Mu: 0,
|
||
Sigma: 1,
|
||
Src: w.src,
|
||
}
|
||
|
||
t := mat.NewTriDense(w.dim, mat.Upper, nil)
|
||
for i := 0; i < w.dim; i++ {
|
||
v := distuv.ChiSquared{
|
||
K: w.nu - float64(i),
|
||
Src: w.src,
|
||
}.Rand()
|
||
t.SetTri(i, i, math.Sqrt(v))
|
||
}
|
||
for i := 0; i < w.dim; i++ {
|
||
for j := i + 1; j < w.dim; j++ {
|
||
t.SetTri(i, j, norm.Rand())
|
||
}
|
||
}
|
||
|
||
t.MulTri(t, &w.upper)
|
||
if c == nil {
|
||
c = &mat.Cholesky{}
|
||
}
|
||
c.SetFromU(t)
|
||
return c
|
||
}
|
||
|
||
// setV computes and stores the covariance matrix of the distribution.
|
||
func (w *Wishart) setV() {
|
||
w.once.Do(func() {
|
||
w.v = mat.NewSymDense(w.dim, nil)
|
||
w.cholv.ToSym(w.v)
|
||
})
|
||
}
|