github.com/gopherd/gonum@v0.0.4/optimize/cmaes.go (about) 1 // Copyright ©2017 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 optimize 6 7 import ( 8 "math" 9 "sort" 10 11 "math/rand" 12 13 "github.com/gopherd/gonum/floats" 14 "github.com/gopherd/gonum/mat" 15 "github.com/gopherd/gonum/stat/distmv" 16 ) 17 18 var _ Method = (*CmaEsChol)(nil) 19 20 // TODO(btracey): If we ever implement the traditional CMA-ES algorithm, provide 21 // the base explanation there, and modify this description to just 22 // describe the differences. 23 24 // CmaEsChol implements the covariance matrix adaptation evolution strategy (CMA-ES) 25 // based on the Cholesky decomposition. The full algorithm is described in 26 // Krause, Oswin, Dídac Rodríguez Arbonès, and Christian Igel. "CMA-ES with 27 // optimal covariance update and storage complexity." Advances in Neural 28 // Information Processing Systems. 2016. 29 // https://papers.nips.cc/paper/6457-cma-es-with-optimal-covariance-update-and-storage-complexity.pdf 30 // CMA-ES is a global optimization method that progressively adapts a population 31 // of samples. CMA-ES combines techniques from local optimization with global 32 // optimization. Specifically, the CMA-ES algorithm uses an initial multivariate 33 // normal distribution to generate a population of input locations. The input locations 34 // with the lowest function values are used to update the parameters of the normal 35 // distribution, a new set of input locations are generated, and this procedure 36 // is iterated until convergence. The initial sampling distribution will have 37 // a mean specified by the initial x location, and a covariance specified by 38 // the InitCholesky field. 39 // 40 // As the normal distribution is progressively updated according to the best samples, 41 // it can be that the mean of the distribution is updated in a gradient-descent 42 // like fashion, followed by a shrinking covariance. 43 // It is recommended that the algorithm be run multiple times (with different 44 // InitMean) to have a better chance of finding the global minimum. 45 // 46 // The CMA-ES-Chol algorithm differs from the standard CMA-ES algorithm in that 47 // it directly updates the Cholesky decomposition of the normal distribution. 48 // This changes the runtime from O(dimension^3) to O(dimension^2*population) 49 // The evolution of the multi-variate normal will be similar to the baseline 50 // CMA-ES algorithm, but the covariance update equation is not identical. 51 // 52 // For more information about the CMA-ES algorithm, see 53 // https://en.wikipedia.org/wiki/CMA-ES 54 // https://arxiv.org/pdf/1604.00772.pdf 55 type CmaEsChol struct { 56 // InitStepSize sets the initial size of the covariance matrix adaptation. 57 // If InitStepSize is 0, a default value of 0.5 is used. InitStepSize cannot 58 // be negative, or CmaEsChol will panic. 59 InitStepSize float64 60 // Population sets the population size for the algorithm. If Population is 61 // 0, a default value of 4 + math.Floor(3*math.Log(float64(dim))) is used. 62 // Population cannot be negative or CmaEsChol will panic. 63 Population int 64 // InitCholesky specifies the Cholesky decomposition of the covariance 65 // matrix for the initial sampling distribution. If InitCholesky is nil, 66 // a default value of I is used. If it is non-nil, then it must have 67 // InitCholesky.Size() be equal to the problem dimension. 68 InitCholesky *mat.Cholesky 69 // StopLogDet sets the threshold for stopping the optimization if the 70 // distribution becomes too peaked. The log determinant is a measure of the 71 // (log) "volume" of the normal distribution, and when it is too small 72 // the samples are almost the same. If the log determinant of the covariance 73 // matrix becomes less than StopLogDet, the optimization run is concluded. 74 // If StopLogDet is 0, a default value of dim*log(1e-16) is used. 75 // If StopLogDet is NaN, the stopping criterion is not used, though 76 // this can cause numeric instabilities in the algorithm. 77 StopLogDet float64 78 // ForgetBest, when true, does not track the best overall function value found, 79 // instead returning the new best sample in each iteration. If ForgetBest 80 // is false, then the minimum value returned will be the lowest across all 81 // iterations, regardless of when that sample was generated. 82 ForgetBest bool 83 // Src allows a random number generator to be supplied for generating samples. 84 // If Src is nil the generator in golang.org/x/math/rand is used. 85 Src rand.Source 86 87 // Fixed algorithm parameters. 88 dim int 89 pop int 90 weights []float64 91 muEff float64 92 cc, cs, c1, cmu, ds float64 93 eChi float64 94 95 // Function data. 96 xs *mat.Dense 97 fs []float64 98 99 // Adaptive algorithm parameters. 100 invSigma float64 // inverse of the sigma parameter 101 pc, ps []float64 102 mean []float64 103 chol mat.Cholesky 104 105 // Overall best. 106 bestX []float64 107 bestF float64 108 109 // Synchronization. 110 sentIdx int 111 receivedIdx int 112 operation chan<- Task 113 updateErr error 114 } 115 116 var ( 117 _ Statuser = (*CmaEsChol)(nil) 118 _ Method = (*CmaEsChol)(nil) 119 ) 120 121 func (cma *CmaEsChol) methodConverged() Status { 122 sd := cma.StopLogDet 123 switch { 124 case math.IsNaN(sd): 125 return NotTerminated 126 case sd == 0: 127 sd = float64(cma.dim) * -36.8413614879 // ln(1e-16) 128 } 129 if cma.chol.LogDet() < sd { 130 return MethodConverge 131 } 132 return NotTerminated 133 } 134 135 // Status returns the status of the method. 136 func (cma *CmaEsChol) Status() (Status, error) { 137 if cma.updateErr != nil { 138 return Failure, cma.updateErr 139 } 140 return cma.methodConverged(), nil 141 } 142 143 func (*CmaEsChol) Uses(has Available) (uses Available, err error) { 144 return has.function() 145 } 146 147 func (cma *CmaEsChol) Init(dim, tasks int) int { 148 if dim <= 0 { 149 panic(nonpositiveDimension) 150 } 151 if tasks < 0 { 152 panic(negativeTasks) 153 } 154 155 // Set fixed algorithm parameters. 156 // Parameter values are from https://arxiv.org/pdf/1604.00772.pdf . 157 cma.dim = dim 158 cma.pop = cma.Population 159 n := float64(dim) 160 if cma.pop == 0 { 161 cma.pop = 4 + int(3*math.Log(n)) // Note the implicit floor. 162 } else if cma.pop < 0 { 163 panic("cma-es-chol: negative population size") 164 } 165 mu := cma.pop / 2 166 cma.weights = resize(cma.weights, mu) 167 for i := range cma.weights { 168 v := math.Log(float64(mu)+0.5) - math.Log(float64(i)+1) 169 cma.weights[i] = v 170 } 171 floats.Scale(1/floats.Sum(cma.weights), cma.weights) 172 cma.muEff = 0 173 for _, v := range cma.weights { 174 cma.muEff += v * v 175 } 176 cma.muEff = 1 / cma.muEff 177 178 cma.cc = (4 + cma.muEff/n) / (n + 4 + 2*cma.muEff/n) 179 cma.cs = (cma.muEff + 2) / (n + cma.muEff + 5) 180 cma.c1 = 2 / ((n+1.3)*(n+1.3) + cma.muEff) 181 cma.cmu = math.Min(1-cma.c1, 2*(cma.muEff-2+1/cma.muEff)/((n+2)*(n+2)+cma.muEff)) 182 cma.ds = 1 + 2*math.Max(0, math.Sqrt((cma.muEff-1)/(n+1))-1) + cma.cs 183 // E[chi] is taken from https://en.wikipedia.org/wiki/CMA-ES (there 184 // listed as E[||N(0,1)||]). 185 cma.eChi = math.Sqrt(n) * (1 - 1.0/(4*n) + 1/(21*n*n)) 186 187 // Allocate memory for function data. 188 cma.xs = mat.NewDense(cma.pop, dim, nil) 189 cma.fs = resize(cma.fs, cma.pop) 190 191 // Allocate and initialize adaptive parameters. 192 cma.invSigma = 1 / cma.InitStepSize 193 if cma.InitStepSize == 0 { 194 cma.invSigma = 10.0 / 3 195 } else if cma.InitStepSize < 0 { 196 panic("cma-es-chol: negative initial step size") 197 } 198 cma.pc = resize(cma.pc, dim) 199 for i := range cma.pc { 200 cma.pc[i] = 0 201 } 202 cma.ps = resize(cma.ps, dim) 203 for i := range cma.ps { 204 cma.ps[i] = 0 205 } 206 cma.mean = resize(cma.mean, dim) // mean location initialized at the start of Run 207 208 if cma.InitCholesky != nil { 209 if cma.InitCholesky.SymmetricDim() != dim { 210 panic("cma-es-chol: incorrect InitCholesky size") 211 } 212 cma.chol.Clone(cma.InitCholesky) 213 } else { 214 // Set the initial Cholesky to I. 215 b := mat.NewDiagDense(dim, nil) 216 for i := 0; i < dim; i++ { 217 b.SetDiag(i, 1) 218 } 219 var chol mat.Cholesky 220 ok := chol.Factorize(b) 221 if !ok { 222 panic("cma-es-chol: bad cholesky. shouldn't happen") 223 } 224 cma.chol = chol 225 } 226 227 cma.bestX = resize(cma.bestX, dim) 228 cma.bestF = math.Inf(1) 229 230 cma.sentIdx = 0 231 cma.receivedIdx = 0 232 cma.operation = nil 233 cma.updateErr = nil 234 t := min(tasks, cma.pop) 235 return t 236 } 237 238 func (cma *CmaEsChol) sendInitTasks(tasks []Task) { 239 for i, task := range tasks { 240 cma.sendTask(i, task) 241 } 242 cma.sentIdx = len(tasks) 243 } 244 245 // sendTask generates a sample and sends the task. It does not update the cma index. 246 func (cma *CmaEsChol) sendTask(idx int, task Task) { 247 task.ID = idx 248 task.Op = FuncEvaluation 249 distmv.NormalRand(cma.xs.RawRowView(idx), cma.mean, &cma.chol, cma.Src) 250 copy(task.X, cma.xs.RawRowView(idx)) 251 cma.operation <- task 252 } 253 254 // bestIdx returns the best index in the functions. Returns -1 if all values 255 // are NaN. 256 func (cma *CmaEsChol) bestIdx() int { 257 best := -1 258 bestVal := math.Inf(1) 259 for i, v := range cma.fs { 260 if math.IsNaN(v) { 261 continue 262 } 263 // Use equality in case somewhere evaluates to +inf. 264 if v <= bestVal { 265 best = i 266 bestVal = v 267 } 268 } 269 return best 270 } 271 272 // findBestAndUpdateTask finds the best task in the current list, updates the 273 // new best overall, and then stores the best location into task. 274 func (cma *CmaEsChol) findBestAndUpdateTask(task Task) Task { 275 // Find and update the best location. 276 // Don't use floats because there may be NaN values. 277 best := cma.bestIdx() 278 bestF := math.NaN() 279 bestX := cma.xs.RawRowView(0) 280 if best != -1 { 281 bestF = cma.fs[best] 282 bestX = cma.xs.RawRowView(best) 283 } 284 if cma.ForgetBest { 285 task.F = bestF 286 copy(task.X, bestX) 287 } else { 288 if bestF < cma.bestF { 289 cma.bestF = bestF 290 copy(cma.bestX, bestX) 291 } 292 task.F = cma.bestF 293 copy(task.X, cma.bestX) 294 } 295 return task 296 } 297 298 func (cma *CmaEsChol) Run(operations chan<- Task, results <-chan Task, tasks []Task) { 299 copy(cma.mean, tasks[0].X) 300 cma.operation = operations 301 // Send the initial tasks. We know there are at most as many tasks as elements 302 // of the population. 303 cma.sendInitTasks(tasks) 304 305 Loop: 306 for { 307 result := <-results 308 switch result.Op { 309 default: 310 panic("unknown operation") 311 case PostIteration: 312 break Loop 313 case MajorIteration: 314 // The last thing we did was update all of the tasks and send the 315 // major iteration. Now we can send a group of tasks again. 316 cma.sendInitTasks(tasks) 317 case FuncEvaluation: 318 cma.receivedIdx++ 319 cma.fs[result.ID] = result.F 320 switch { 321 case cma.sentIdx < cma.pop: 322 // There are still tasks to evaluate. Send the next. 323 cma.sendTask(cma.sentIdx, result) 324 cma.sentIdx++ 325 case cma.receivedIdx < cma.pop: 326 // All the tasks have been sent, but not all of them have been received. 327 // Need to wait until all are back. 328 continue Loop 329 default: 330 // All of the evaluations have been received. 331 if cma.receivedIdx != cma.pop { 332 panic("bad logic") 333 } 334 cma.receivedIdx = 0 335 cma.sentIdx = 0 336 337 task := cma.findBestAndUpdateTask(result) 338 // Update the parameters and send a MajorIteration or a convergence. 339 err := cma.update() 340 // Kill the existing data. 341 for i := range cma.fs { 342 cma.fs[i] = math.NaN() 343 cma.xs.Set(i, 0, math.NaN()) 344 } 345 switch { 346 case err != nil: 347 cma.updateErr = err 348 task.Op = MethodDone 349 case cma.methodConverged() != NotTerminated: 350 task.Op = MethodDone 351 default: 352 task.Op = MajorIteration 353 task.ID = -1 354 } 355 operations <- task 356 } 357 } 358 } 359 360 // Been told to stop. Clean up. 361 // Need to see best of our evaluated tasks so far. Should instead just 362 // collect, then see. 363 for task := range results { 364 switch task.Op { 365 case MajorIteration: 366 case FuncEvaluation: 367 cma.fs[task.ID] = task.F 368 default: 369 panic("unknown operation") 370 } 371 } 372 // Send the new best value if the evaluation is better than any we've 373 // found so far. Keep this separate from findBestAndUpdateTask so that 374 // we only send an iteration if we find a better location. 375 if !cma.ForgetBest { 376 best := cma.bestIdx() 377 if best != -1 && cma.fs[best] < cma.bestF { 378 task := tasks[0] 379 task.F = cma.fs[best] 380 copy(task.X, cma.xs.RawRowView(best)) 381 task.Op = MajorIteration 382 task.ID = -1 383 operations <- task 384 } 385 } 386 close(operations) 387 } 388 389 // update computes the new parameters (mean, cholesky, etc.). Does not update 390 // any of the synchronization parameters (taskIdx). 391 func (cma *CmaEsChol) update() error { 392 // Sort the function values to find the elite samples. 393 ftmp := make([]float64, cma.pop) 394 copy(ftmp, cma.fs) 395 indexes := make([]int, cma.pop) 396 for i := range indexes { 397 indexes[i] = i 398 } 399 sort.Sort(bestSorter{F: ftmp, Idx: indexes}) 400 401 meanOld := make([]float64, len(cma.mean)) 402 copy(meanOld, cma.mean) 403 404 // m_{t+1} = \sum_{i=1}^mu w_i x_i 405 for i := range cma.mean { 406 cma.mean[i] = 0 407 } 408 for i, w := range cma.weights { 409 idx := indexes[i] // index of teh 1337 sample. 410 floats.AddScaled(cma.mean, w, cma.xs.RawRowView(idx)) 411 } 412 meanDiff := make([]float64, len(cma.mean)) 413 floats.SubTo(meanDiff, cma.mean, meanOld) 414 415 // p_{c,t+1} = (1-c_c) p_{c,t} + \sqrt(c_c*(2-c_c)*mueff) (m_{t+1}-m_t)/sigma_t 416 floats.Scale(1-cma.cc, cma.pc) 417 scaleC := math.Sqrt(cma.cc*(2-cma.cc)*cma.muEff) * cma.invSigma 418 floats.AddScaled(cma.pc, scaleC, meanDiff) 419 420 // p_{sigma, t+1} = (1-c_sigma) p_{sigma,t} + \sqrt(c_s*(2-c_s)*mueff) A_t^-1 (m_{t+1}-m_t)/sigma_t 421 floats.Scale(1-cma.cs, cma.ps) 422 // First compute A_t^-1 (m_{t+1}-m_t), then add the scaled vector. 423 tmp := make([]float64, cma.dim) 424 tmpVec := mat.NewVecDense(cma.dim, tmp) 425 diffVec := mat.NewVecDense(cma.dim, meanDiff) 426 err := tmpVec.SolveVec(cma.chol.RawU().T(), diffVec) 427 if err != nil { 428 return err 429 } 430 scaleS := math.Sqrt(cma.cs*(2-cma.cs)*cma.muEff) * cma.invSigma 431 floats.AddScaled(cma.ps, scaleS, tmp) 432 433 // Compute the update to A. 434 scaleChol := 1 - cma.c1 - cma.cmu 435 if scaleChol == 0 { 436 scaleChol = math.SmallestNonzeroFloat64 // enough to kill the old data, but still non-zero. 437 } 438 cma.chol.Scale(scaleChol, &cma.chol) 439 cma.chol.SymRankOne(&cma.chol, cma.c1, mat.NewVecDense(cma.dim, cma.pc)) 440 for i, w := range cma.weights { 441 idx := indexes[i] 442 floats.SubTo(tmp, cma.xs.RawRowView(idx), meanOld) 443 cma.chol.SymRankOne(&cma.chol, cma.cmu*w*cma.invSigma, tmpVec) 444 } 445 446 // sigma_{t+1} = sigma_t exp(c_sigma/d_sigma * norm(p_{sigma,t+1}/ E[chi] -1) 447 normPs := floats.Norm(cma.ps, 2) 448 cma.invSigma /= math.Exp(cma.cs / cma.ds * (normPs/cma.eChi - 1)) 449 return nil 450 } 451 452 type bestSorter struct { 453 F []float64 454 Idx []int 455 } 456 457 func (b bestSorter) Len() int { 458 return len(b.F) 459 } 460 func (b bestSorter) Less(i, j int) bool { 461 return b.F[i] < b.F[j] 462 } 463 func (b bestSorter) Swap(i, j int) { 464 b.F[i], b.F[j] = b.F[j], b.F[i] 465 b.Idx[i], b.Idx[j] = b.Idx[j], b.Idx[i] 466 }