github.com/gopherd/gonum@v0.0.4/stat/distmv/studentst.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 distmv 6 7 import ( 8 "math" 9 "sort" 10 11 "math/rand" 12 13 "github.com/gopherd/gonum/container/intsets" 14 "github.com/gopherd/gonum/floats" 15 "github.com/gopherd/gonum/mat" 16 "github.com/gopherd/gonum/stat" 17 "github.com/gopherd/gonum/stat/distuv" 18 ) 19 20 // StudentsT is a multivariate Student's T distribution. It is a distribution over 21 // ℝ^n with the probability density 22 // p(y) = (Γ((ν+n)/2) / Γ(ν/2)) * (νπ)^(-n/2) * |Ʃ|^(-1/2) * 23 // (1 + 1/ν * (y-μ)ᵀ * Ʃ^-1 * (y-μ))^(-(ν+n)/2) 24 // where ν is a scalar greater than 2, μ is a vector in ℝ^n, and Ʃ is an n×n 25 // symmetric positive definite matrix. 26 // 27 // In this distribution, ν sets the spread of the distribution, similar to 28 // the degrees of freedom in a univariate Student's T distribution. As ν → ∞, 29 // the distribution approaches a multi-variate normal distribution. 30 // μ is the mean of the distribution, and the covariance is ν/(ν-2)*Ʃ. 31 // 32 // See https://en.wikipedia.org/wiki/Student%27s_t-distribution and 33 // http://users.isy.liu.se/en/rt/roth/student.pdf for more information. 34 type StudentsT struct { 35 nu float64 36 mu []float64 37 // If src is altered, rnd must be updated. 38 src rand.Source 39 rnd *rand.Rand 40 41 sigma mat.SymDense // only stored if needed 42 43 chol mat.Cholesky 44 lower mat.TriDense 45 logSqrtDet float64 46 dim int 47 } 48 49 // NewStudentsT creates a new StudentsT with the given nu, mu, and sigma 50 // parameters. 51 // 52 // NewStudentsT panics if len(mu) == 0, or if len(mu) != sigma.SymmetricDim(). If 53 // the covariance matrix is not positive-definite, nil is returned and ok is false. 54 func NewStudentsT(mu []float64, sigma mat.Symmetric, nu float64, src rand.Source) (dist *StudentsT, ok bool) { 55 if len(mu) == 0 { 56 panic(badZeroDimension) 57 } 58 dim := sigma.SymmetricDim() 59 if dim != len(mu) { 60 panic(badSizeMismatch) 61 } 62 63 s := &StudentsT{ 64 nu: nu, 65 mu: make([]float64, dim), 66 dim: dim, 67 src: src, 68 } 69 if src != nil { 70 s.rnd = rand.New(src) 71 } 72 copy(s.mu, mu) 73 74 ok = s.chol.Factorize(sigma) 75 if !ok { 76 return nil, false 77 } 78 s.sigma = *mat.NewSymDense(dim, nil) 79 s.sigma.CopySym(sigma) 80 s.chol.LTo(&s.lower) 81 s.logSqrtDet = 0.5 * s.chol.LogDet() 82 return s, true 83 } 84 85 // ConditionStudentsT returns the Student's T distribution that is the receiver 86 // conditioned on the input evidence, and the success of the operation. 87 // The returned Student's T has dimension 88 // n - len(observed), where n is the dimension of the original receiver. 89 // The dimension order is preserved during conditioning, so if the value 90 // of dimension 1 is observed, the returned normal represents dimensions {0, 2, ...} 91 // of the original Student's T distribution. 92 // 93 // ok indicates whether there was a failure during the update. If ok is false 94 // the operation failed and dist is not usable. 95 // Mathematically this is impossible, but can occur with finite precision arithmetic. 96 func (s *StudentsT) ConditionStudentsT(observed []int, values []float64, src rand.Source) (dist *StudentsT, ok bool) { 97 if len(observed) == 0 { 98 panic("studentst: no observed value") 99 } 100 if len(observed) != len(values) { 101 panic(badInputLength) 102 } 103 104 for _, v := range observed { 105 if v < 0 || v >= s.dim { 106 panic("studentst: observed value out of bounds") 107 } 108 } 109 110 newNu, newMean, newSigma := studentsTConditional(observed, values, s.nu, s.mu, &s.sigma) 111 if newMean == nil { 112 return nil, false 113 } 114 115 return NewStudentsT(newMean, newSigma, newNu, src) 116 117 } 118 119 // studentsTConditional updates a Student's T distribution based on the observed samples 120 // (see documentation for the public function). The Gaussian conditional update 121 // is treated as a special case when nu == math.Inf(1). 122 func studentsTConditional(observed []int, values []float64, nu float64, mu []float64, sigma mat.Symmetric) (newNu float64, newMean []float64, newSigma *mat.SymDense) { 123 dim := len(mu) 124 ob := len(observed) 125 126 unobserved := findUnob(observed, dim) 127 128 unob := len(unobserved) 129 if unob == 0 { 130 panic("stat: all dimensions observed") 131 } 132 133 mu1 := make([]float64, unob) 134 for i, v := range unobserved { 135 mu1[i] = mu[v] 136 } 137 mu2 := make([]float64, ob) // really v - mu2 138 for i, v := range observed { 139 mu2[i] = values[i] - mu[v] 140 } 141 142 var sigma11, sigma22 mat.SymDense 143 sigma11.SubsetSym(sigma, unobserved) 144 sigma22.SubsetSym(sigma, observed) 145 146 sigma21 := mat.NewDense(ob, unob, nil) 147 for i, r := range observed { 148 for j, c := range unobserved { 149 v := sigma.At(r, c) 150 sigma21.Set(i, j, v) 151 } 152 } 153 154 var chol mat.Cholesky 155 ok := chol.Factorize(&sigma22) 156 if !ok { 157 return math.NaN(), nil, nil 158 } 159 160 // Compute mu_1 + sigma_{2,1}ᵀ * sigma_{2,2}^-1 (v - mu_2). 161 v := mat.NewVecDense(ob, mu2) 162 var tmp, tmp2 mat.VecDense 163 err := chol.SolveVecTo(&tmp, v) 164 if err != nil { 165 return math.NaN(), nil, nil 166 } 167 tmp2.MulVec(sigma21.T(), &tmp) 168 169 for i := range mu1 { 170 mu1[i] += tmp2.At(i, 0) 171 } 172 173 // Compute tmp4 = sigma_{2,1}ᵀ * sigma_{2,2}^-1 * sigma_{2,1}. 174 // TODO(btracey): Should this be a method of SymDense? 175 var tmp3, tmp4 mat.Dense 176 err = chol.SolveTo(&tmp3, sigma21) 177 if err != nil { 178 return math.NaN(), nil, nil 179 } 180 tmp4.Mul(sigma21.T(), &tmp3) 181 182 // Compute sigma_{1,1} - tmp4 183 // TODO(btracey): If tmp4 can constructed with a method, then this can be 184 // replaced with SubSym. 185 for i := 0; i < len(unobserved); i++ { 186 for j := i; j < len(unobserved); j++ { 187 v := sigma11.At(i, j) 188 sigma11.SetSym(i, j, v-tmp4.At(i, j)) 189 } 190 } 191 192 // The computed variables are accurate for a Normal. 193 if math.IsInf(nu, 1) { 194 return nu, mu1, &sigma11 195 } 196 197 // Compute beta = (v - mu_2)ᵀ * sigma_{2,2}^-1 * (v - mu_2)ᵀ 198 beta := mat.Dot(v, &tmp) 199 200 // Scale the covariance matrix 201 sigma11.ScaleSym((nu+beta)/(nu+float64(ob)), &sigma11) 202 203 return nu + float64(ob), mu1, &sigma11 204 } 205 206 // findUnob returns the unobserved variables (the complementary set to observed). 207 // findUnob panics if any value repeated in observed. 208 func findUnob(observed []int, dim int) (unobserved []int) { 209 var setOb intsets.Sparse 210 for _, v := range observed { 211 setOb.Insert(v) 212 } 213 var setAll intsets.Sparse 214 for i := 0; i < dim; i++ { 215 setAll.Insert(i) 216 } 217 var setUnob intsets.Sparse 218 setUnob.Difference(&setAll, &setOb) 219 unobserved = setUnob.AppendTo(nil) 220 sort.Ints(unobserved) 221 return unobserved 222 } 223 224 // CovarianceMatrix calculates the covariance matrix of the distribution, 225 // storing the result in dst. Upon return, the value at element {i, j} of the 226 // covariance matrix is equal to the covariance of the i^th and j^th variables. 227 // covariance(i, j) = E[(x_i - E[x_i])(x_j - E[x_j])] 228 // If the dst matrix is empty it will be resized to the correct dimensions, 229 // otherwise dst must match the dimension of the receiver or CovarianceMatrix 230 // will panic. 231 func (st *StudentsT) CovarianceMatrix(dst *mat.SymDense) { 232 if dst.IsEmpty() { 233 *dst = *(dst.GrowSym(st.dim).(*mat.SymDense)) 234 } else if dst.SymmetricDim() != st.dim { 235 panic("studentst: input matrix size mismatch") 236 } 237 dst.CopySym(&st.sigma) 238 dst.ScaleSym(st.nu/(st.nu-2), dst) 239 } 240 241 // Dim returns the dimension of the distribution. 242 func (s *StudentsT) Dim() int { 243 return s.dim 244 } 245 246 // LogProb computes the log of the pdf of the point x. 247 func (s *StudentsT) LogProb(y []float64) float64 { 248 if len(y) != s.dim { 249 panic(badInputLength) 250 } 251 252 nu := s.nu 253 n := float64(s.dim) 254 lg1, _ := math.Lgamma((nu + n) / 2) 255 lg2, _ := math.Lgamma(nu / 2) 256 257 t1 := lg1 - lg2 - n/2*math.Log(nu*math.Pi) - s.logSqrtDet 258 259 mahal := stat.Mahalanobis(mat.NewVecDense(len(y), y), mat.NewVecDense(len(s.mu), s.mu), &s.chol) 260 mahal *= mahal 261 return t1 - ((nu+n)/2)*math.Log(1+mahal/nu) 262 } 263 264 // MarginalStudentsT returns the marginal distribution of the given input variables, 265 // and the success of the operation. 266 // That is, MarginalStudentsT returns 267 // p(x_i) = \int_{x_o} p(x_i | x_o) p(x_o) dx_o 268 // where x_i are the dimensions in the input, and x_o are the remaining dimensions. 269 // See https://en.wikipedia.org/wiki/Marginal_distribution for more information. 270 // 271 // The input src is passed to the created StudentsT. 272 // 273 // ok indicates whether there was a failure during the marginalization. If ok is false 274 // the operation failed and dist is not usable. 275 // Mathematically this is impossible, but can occur with finite precision arithmetic. 276 func (s *StudentsT) MarginalStudentsT(vars []int, src rand.Source) (dist *StudentsT, ok bool) { 277 newMean := make([]float64, len(vars)) 278 for i, v := range vars { 279 newMean[i] = s.mu[v] 280 } 281 var newSigma mat.SymDense 282 newSigma.SubsetSym(&s.sigma, vars) 283 return NewStudentsT(newMean, &newSigma, s.nu, src) 284 } 285 286 // MarginalStudentsTSingle returns the marginal distribution of the given input variable. 287 // That is, MarginalStudentsTSingle returns 288 // p(x_i) = \int_{x_o} p(x_i | x_o) p(x_o) dx_o 289 // where i is the input index, and x_o are the remaining dimensions. 290 // See https://en.wikipedia.org/wiki/Marginal_distribution for more information. 291 // 292 // The input src is passed to the call to NewStudentsT. 293 func (s *StudentsT) MarginalStudentsTSingle(i int, src rand.Source) distuv.StudentsT { 294 return distuv.StudentsT{ 295 Mu: s.mu[i], 296 Sigma: math.Sqrt(s.sigma.At(i, i)), 297 Nu: s.nu, 298 Src: src, 299 } 300 } 301 302 // TODO(btracey): Implement marginal single. Need to modify univariate StudentsT 303 // to be three-parameter. 304 305 // Mean returns the mean of the probability distribution at x. If the 306 // input argument is nil, a new slice will be allocated, otherwise the result 307 // will be put in-place into the receiver. 308 func (s *StudentsT) Mean(x []float64) []float64 { 309 x = reuseAs(x, s.dim) 310 copy(x, s.mu) 311 return x 312 } 313 314 // Nu returns the degrees of freedom parameter of the distribution. 315 func (s *StudentsT) Nu() float64 { 316 return s.nu 317 } 318 319 // Prob computes the value of the probability density function at x. 320 func (s *StudentsT) Prob(y []float64) float64 { 321 return math.Exp(s.LogProb(y)) 322 } 323 324 // Rand generates a random number according to the distributon. 325 // If the input slice is nil, new memory is allocated, otherwise the result is stored 326 // in place. 327 func (s *StudentsT) Rand(x []float64) []float64 { 328 // If Y is distributed according to N(0,Sigma), and U is chi^2 with 329 // parameter ν, then 330 // X = mu + Y * sqrt(nu / U) 331 // X is distributed according to this distribution. 332 333 // Generate Y. 334 x = reuseAs(x, s.dim) 335 tmp := make([]float64, s.dim) 336 if s.rnd == nil { 337 for i := range x { 338 tmp[i] = rand.NormFloat64() 339 } 340 } else { 341 for i := range x { 342 tmp[i] = s.rnd.NormFloat64() 343 } 344 } 345 xVec := mat.NewVecDense(s.dim, x) 346 tmpVec := mat.NewVecDense(s.dim, tmp) 347 xVec.MulVec(&s.lower, tmpVec) 348 349 u := distuv.ChiSquared{K: s.nu, Src: s.src}.Rand() 350 floats.Scale(math.Sqrt(s.nu/u), x) 351 352 floats.Add(x, s.mu) 353 return x 354 }