github.com/gopherd/gonum@v0.0.4/stat/distmat/wishart.go (about)

     1  // Copyright ©2016 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 distmat
     6  
     7  import (
     8  	"math"
     9  	"sync"
    10  
    11  	"math/rand"
    12  
    13  	"github.com/gopherd/gonum/mat"
    14  	"github.com/gopherd/gonum/mathext"
    15  	"github.com/gopherd/gonum/stat/distuv"
    16  )
    17  
    18  // Wishart is a distribution over d×d positive symmetric definite matrices. It
    19  // is parametrized by a scalar degrees of freedom parameter ν and a d×d positive
    20  // definite matrix V.
    21  //
    22  // The Wishart PDF is given by
    23  //  p(X) = [|X|^((ν-d-1)/2) * exp(-tr(V^-1 * X)/2)] / [2^(ν*d/2) * |V|^(ν/2) * Γ_d(ν/2)]
    24  // where X is a d×d PSD matrix, ν > d-1, |·| denotes the determinant, tr is the
    25  // trace and Γ_d is the multivariate gamma function.
    26  //
    27  // See https://en.wikipedia.org/wiki/Wishart_distribution for more information.
    28  type Wishart struct {
    29  	nu  float64
    30  	src rand.Source
    31  
    32  	dim     int
    33  	cholv   mat.Cholesky
    34  	logdetv float64
    35  	upper   mat.TriDense
    36  
    37  	once sync.Once
    38  	v    *mat.SymDense // only stored if needed
    39  }
    40  
    41  // NewWishart returns a new Wishart distribution with the given shape matrix and
    42  // degrees of freedom parameter. NewWishart returns whether the creation was
    43  // successful.
    44  //
    45  // NewWishart panics if nu <= d - 1 where d is the order of v.
    46  func NewWishart(v mat.Symmetric, nu float64, src rand.Source) (*Wishart, bool) {
    47  	dim := v.SymmetricDim()
    48  	if nu <= float64(dim-1) {
    49  		panic("wishart: nu must be greater than dim-1")
    50  	}
    51  	var chol mat.Cholesky
    52  	ok := chol.Factorize(v)
    53  	if !ok {
    54  		return nil, false
    55  	}
    56  
    57  	var u mat.TriDense
    58  	chol.UTo(&u)
    59  
    60  	w := &Wishart{
    61  		nu:  nu,
    62  		src: src,
    63  
    64  		dim:     dim,
    65  		cholv:   chol,
    66  		logdetv: chol.LogDet(),
    67  		upper:   u,
    68  	}
    69  	return w, true
    70  }
    71  
    72  // MeanSymTo calculates the mean matrix of the distribution in and stores it in dst.
    73  // If dst is empty, it is resized to be an d×d symmetric matrix where d is the order
    74  // of the receiver. When dst is non-empty, MeanSymTo panics if dst is not d×d.
    75  func (w *Wishart) MeanSymTo(dst *mat.SymDense) {
    76  	if dst.IsEmpty() {
    77  		dst.ReuseAsSym(w.dim)
    78  	} else if dst.SymmetricDim() != w.dim {
    79  		panic(badDim)
    80  	}
    81  	w.setV()
    82  	dst.CopySym(w.v)
    83  	dst.ScaleSym(w.nu, dst)
    84  }
    85  
    86  // ProbSym returns the probability of the symmetric matrix x. If x is not positive
    87  // definite (the Cholesky decomposition fails), it has 0 probability.
    88  func (w *Wishart) ProbSym(x mat.Symmetric) float64 {
    89  	return math.Exp(w.LogProbSym(x))
    90  }
    91  
    92  // LogProbSym returns the log of the probability of the input symmetric matrix.
    93  //
    94  // LogProbSym returns -∞ if the input matrix is not positive definite (the Cholesky
    95  // decomposition fails).
    96  func (w *Wishart) LogProbSym(x mat.Symmetric) float64 {
    97  	dim := x.SymmetricDim()
    98  	if dim != w.dim {
    99  		panic(badDim)
   100  	}
   101  	var chol mat.Cholesky
   102  	ok := chol.Factorize(x)
   103  	if !ok {
   104  		return math.Inf(-1)
   105  	}
   106  	return w.logProbSymChol(&chol)
   107  }
   108  
   109  // LogProbSymChol returns the log of the probability of the input symmetric matrix
   110  // given its Cholesky decomposition.
   111  func (w *Wishart) LogProbSymChol(cholX *mat.Cholesky) float64 {
   112  	dim := cholX.SymmetricDim()
   113  	if dim != w.dim {
   114  		panic(badDim)
   115  	}
   116  	return w.logProbSymChol(cholX)
   117  }
   118  
   119  func (w *Wishart) logProbSymChol(cholX *mat.Cholesky) float64 {
   120  	// The PDF is
   121  	//  p(X) = [|X|^((ν-d-1)/2) * exp(-tr(V^-1 * X)/2)] / [2^(ν*d/2) * |V|^(ν/2) * Γ_d(ν/2)]
   122  	// The LogPDF is thus
   123  	//  (ν-d-1)/2 * log(|X|) - tr(V^-1 * X)/2  - (ν*d/2)*log(2) - ν/2 * log(|V|) - log(Γ_d(ν/2))
   124  	logdetx := cholX.LogDet()
   125  
   126  	// Compute tr(V^-1 * X), using the fact that X = Uᵀ * U.
   127  	var u mat.TriDense
   128  	cholX.UTo(&u)
   129  
   130  	var vinvx mat.Dense
   131  	err := w.cholv.SolveTo(&vinvx, u.T())
   132  	if err != nil {
   133  		return math.Inf(-1)
   134  	}
   135  	vinvx.Mul(&vinvx, &u)
   136  	tr := mat.Trace(&vinvx)
   137  
   138  	fnu := float64(w.nu)
   139  	fdim := float64(w.dim)
   140  
   141  	return 0.5*((fnu-fdim-1)*logdetx-tr-fnu*fdim*math.Ln2-fnu*w.logdetv) - mathext.MvLgamma(0.5*fnu, w.dim)
   142  }
   143  
   144  // RandSymTo generates a random symmetric matrix from the distribution.
   145  // If dst is empty, it is resized to be an d×d symmetric matrix where d is the order
   146  // of the receiver. When dst is non-empty, RandSymTo panics if dst is not d×d.
   147  func (w *Wishart) RandSymTo(dst *mat.SymDense) {
   148  	var c mat.Cholesky
   149  	w.RandCholTo(&c)
   150  	c.ToSym(dst)
   151  }
   152  
   153  // RandCholTo generates the Cholesky decomposition of a random matrix from the distribution.
   154  // If dst is empty, it is resized to be an d×d symmetric matrix where d is the order
   155  // of the receiver. When dst is non-empty, RandCholTo panics if dst is not d×d.
   156  func (w *Wishart) RandCholTo(dst *mat.Cholesky) {
   157  	// TODO(btracey): Modify the code if the underlying data from dst is exposed
   158  	// to avoid the dim^2 allocation here.
   159  
   160  	// Use the Bartlett Decomposition, which says that
   161  	//  X ~ L A Aᵀ Lᵀ
   162  	// Where A is a lower triangular matrix in which the diagonal of A is
   163  	// generated from the square roots of χ^2 random variables, and the
   164  	// off-diagonals are generated from standard normal variables.
   165  	// The above gives the cholesky decomposition of X, where L_x = L A.
   166  	//
   167  	// mat works with the upper triagular decomposition, so we would like to do
   168  	// the same. We can instead say that
   169  	//  U_x = L_xᵀ = (L * A)ᵀ = Aᵀ * Lᵀ = Aᵀ * U
   170  	// Instead, generate Aᵀ, by using the procedure above, except as an upper
   171  	// triangular matrix.
   172  	norm := distuv.Normal{
   173  		Mu:    0,
   174  		Sigma: 1,
   175  		Src:   w.src,
   176  	}
   177  
   178  	t := mat.NewTriDense(w.dim, mat.Upper, nil)
   179  	for i := 0; i < w.dim; i++ {
   180  		v := distuv.ChiSquared{
   181  			K:   w.nu - float64(i),
   182  			Src: w.src,
   183  		}.Rand()
   184  		t.SetTri(i, i, math.Sqrt(v))
   185  	}
   186  	for i := 0; i < w.dim; i++ {
   187  		for j := i + 1; j < w.dim; j++ {
   188  			t.SetTri(i, j, norm.Rand())
   189  		}
   190  	}
   191  
   192  	t.MulTri(t, &w.upper)
   193  	dst.SetFromU(t)
   194  }
   195  
   196  // setV computes and stores the covariance matrix of the distribution.
   197  func (w *Wishart) setV() {
   198  	w.once.Do(func() {
   199  		w.v = mat.NewSymDense(w.dim, nil)
   200  		w.cholv.ToSym(w.v)
   201  	})
   202  }