math-optim/algo/randomSearch.go

200 lines
5.3 KiB
Go
Raw Normal View History

2023-01-12 23:35:51 +01:00
// Copyright 2023 wanderer <a_mirre at utb dot cz>
2022-06-14 22:34:52 +02:00
// SPDX-License-Identifier: GPL-3.0-or-later
package algo
import (
"fmt"
2022-06-17 01:54:30 +02:00
"log"
2022-06-14 22:34:52 +02:00
"os"
"sort"
"time"
2022-06-17 01:54:30 +02:00
"git.dotya.ml/wanderer/math-optim/bench"
2022-06-18 05:27:10 +02:00
"git.dotya.ml/wanderer/math-optim/stats"
"golang.org/x/exp/rand"
2022-06-17 01:54:30 +02:00
"gonum.org/v1/gonum/stat/distuv"
2022-06-14 22:34:52 +02:00
)
func getRandomSearchLogPrefix() string {
return " ***  random search:"
}
func fmtRandomSearchOut(input string) string {
return getRandomSearchLogPrefix() + " " + input
}
func printRandomSearch(input string) {
if _, err := fmt.Fprintln(os.Stderr, fmtRandomSearchOut(input)); err != nil {
fmt.Fprintf(
os.Stdout,
getRandomSearchLogPrefix(),
"error while printing to stderr: %q\n * original message was: %q",
err, input,
)
}
2022-06-14 22:34:52 +02:00
}
2022-06-17 01:54:30 +02:00
func genValsRandomSearch(dimens uint, vals []float64, uniform *distuv.Uniform) {
for i := uint(0); i < dimens; i++ {
// using Uniform.Rand from gonum's stat/distuv package.
// https://pkg.go.dev/gonum.org/v1/gonum/stat/distuv#Uniform.Rand
// boundaries are already set at this point.
vals[i] = uniform.Rand()
}
}
// singleRandomSearch performs a single iteration of the 'RandomSearch'
// algorithm. it takes a couple of arguments:
2022-06-17 01:54:30 +02:00
// * dimens uint: number of dimensions of the objective function
// * f func([]float64) float64: bench func to execute (see Functions map in
// bench/functions.go)
// * uniformDist distuv.Uniform: uniform distribution representation with the
// min/max bounds already set to function-specific limits.
2022-07-07 17:55:47 +02:00
func singleRandomSearch(dimens uint, f func([]float64) float64, uniformDist distuv.Uniform) ([]float64, float64) {
vals := make([]float64, dimens)
2022-06-17 01:54:30 +02:00
genValsRandomSearch(dimens, vals, &uniformDist)
// result of the bench function.
res := f(vals)
2022-06-17 01:54:30 +02:00
return vals, res
}
2022-07-09 16:19:56 +02:00
func RandomSearchNG(maxFES, benchMinIters int, theD []int, benchFunc string, ch chan []stats.Stats) {
// perform basic sanity checks.
if maxFES <= 0 {
log.Fatalln(fmtRandomSearchOut("maxFES cannot be <= 0, bailing"))
} else if benchMinIters <= 0 {
log.Fatalln(fmtRandomSearchOut("benchMinIters cannot be <= 0, bailing"))
} else if _, ok := bench.Functions[benchFunc]; !ok {
log.Fatalln(fmtRandomSearchOut(
"unknown benchFunc used: '" + benchFunc + "', bailing",
))
2022-06-17 01:54:30 +02:00
}
for i := range theD {
if theD[i] <= 0 {
log.Fatalln(fmtRandomSearchOut(" no dimension in D can be <= 0, bailing"))
}
}
2022-06-17 01:54:30 +02:00
// use func-local vars.
var (
fes int
localD []int
minIters int
randomSearchStats []stats.Stats
)
fes = maxFES
localD = theD
minIters = benchMinIters
2022-07-07 17:55:47 +02:00
// create a continuous uniform distribution representation.
uniformDist := &distuv.Uniform{
Src: rand.NewSource(uint64(
time.Now().
UnixNano(),
)),
}
rsMeans := &stats.AlgoBenchMean{
Algo: "Random Search",
BenchMeans: make([]stats.BenchMean, 0, len(localD)),
}
// iterate over whatever was passed to us with theD - dimens slice.
for _, dimens := range localD {
randomSearchStatDimX := &stats.Stats{
2022-06-18 05:27:10 +02:00
Algo: "Random Search",
Dimens: dimens,
2022-06-18 05:27:10 +02:00
Iterations: minIters,
Generations: fes,
2022-06-18 05:27:10 +02:00
}
funcStats := &stats.FuncStats{BenchName: benchFunc}
benchFuncParams := bench.FunctionParams[benchFunc]
dimXMean := &stats.BenchMean{
Bench: benchFunc,
Dimens: dimens,
Iterations: minIters,
Generations: fes,
// not applicable to Random Search...
Neighbours: -1,
}
2022-06-18 05:27:10 +02:00
2022-07-07 17:55:47 +02:00
// set min/max bounds.
uniformDist.Min = benchFuncParams.Min()
uniformDist.Max = benchFuncParams.Max()
2022-06-25 21:55:36 +02:00
printRandomSearch("running bench \"" + benchFunc + "\" for " +
fmt.Sprint(randomSearchStatDimX.Dimens) + "D")
funcStats.BenchResults = make([]stats.BenchRound, minIters)
2022-06-18 05:27:10 +02:00
// perform the while dance 'minIters' times for "statistical relevance"
2022-06-18 00:08:10 +02:00
for iter := 0; iter < minIters; iter++ {
// have a fresh bestResult for each of the 'minIters' runs.
var bestResult float64
// set current iteration in funcStats.
funcStats.BenchResults[iter].Iteration = iter
// run the benchmarking function 'fes' times.
for i := 0; i < fes; i++ {
_, r := singleRandomSearch(
uint(dimens),
bench.Functions[benchFunc],
2022-07-07 17:55:47 +02:00
*uniformDist,
2022-06-18 05:27:10 +02:00
)
// is a switch efficient, or should an if statement be used..?
// TODO(me): perhaps benchmark this...
switch i {
case 0:
// if it's our first, bestResult is the result we just got.
bestResult = r
default:
// since we're minimising, the lower 'r' is the better.
if r < bestResult {
bestResult = r
}
}
// save the 'best' result, since we only care about those.
funcStats.BenchResults[iter].Results = append(
funcStats.BenchResults[iter].Results,
bestResult,
2022-06-18 05:27:10 +02:00
)
}
}
// get mean vals.
dimXMean.MeanVals = stats.GetMeanVals(funcStats.BenchResults, fes)
// save to funcStats, too.
funcStats.MeanVals = dimXMean.MeanVals
// save cumulative results of 'minIters' runs.
randomSearchStatDimX.BenchFuncStats = append(
randomSearchStatDimX.BenchFuncStats,
*funcStats,
)
2022-06-18 05:27:10 +02:00
// save stats for each dimension to a stats slice.
randomSearchStats = append(randomSearchStats, *randomSearchStatDimX)
// save to AlgoMeans
rsMeans.BenchMeans = append(rsMeans.BenchMeans, *dimXMean)
2022-06-17 01:54:30 +02:00
}
sort.Sort(rsMeans)
// export AlgoMeans.
mu.Lock()
meanStats.AlgoMeans = append(meanStats.AlgoMeans, *rsMeans)
mu.Unlock()
2022-07-09 16:19:56 +02:00
ch <- randomSearchStats
2022-06-17 01:54:30 +02:00
}