math-optim/algo/stochasticHillClimbing.go
leo 68b14f9960
All checks were successful
continuous-integration/drone/push Build is passing
chore: add all updates, sort out later
2022-12-24 11:30:22 +01:00

360 lines
9.8 KiB
Go

// Copyright 2022 wanderer <a_mirre at utb dot cz>
// SPDX-License-Identifier: GPL-3.0-or-later
package algo
import (
"fmt"
"log"
"math"
"os"
"sort"
"time"
"git.dotya.ml/wanderer/math-optim/bench"
"git.dotya.ml/wanderer/math-optim/stats"
"golang.org/x/exp/rand"
"gonum.org/v1/gonum/stat/distuv"
)
// neighbour is an alias for a float64 slice used for more natural handling of
// the underlying data.
type neighbour []float64
func getSHCLogPrefix() string {
return " ***  stochastic hill climbing:"
}
func fmtSHCOut(input string) string {
return getSHCLogPrefix() + " " + input
}
func printSHC(input string) {
if _, err := fmt.Fprintln(os.Stderr, fmtSHCOut(input)); err != nil {
fmt.Fprintf(
os.Stdout,
getSHCLogPrefix(),
"error while printing to stderr: %q\n * original message was: %q",
err, input,
)
}
}
func initValsSHC(dimens uint, vals []float64, uniform *distuv.Uniform) {
for i := uint(0); i < dimens; i++ {
vals[i] = uniform.Rand()
}
}
// selectBestNeighbour evaluates the randomly generated neighbours passed to it
// as a parameter and points to the best one (the one yielding the greatest
// progress in a favourable way).
func selectBestNeighbour(neighbours []neighbour, benchFuncName string) ([]float64, float64) {
// bestNeighbour is selected based on the value of the bench function it yields.
var bestNeighbour []float64
// bestSolution is used to store the best bench func name value.
var bestSolution float64
// f is the actual bench function, based on the name.
f := bench.Functions[benchFuncName]
// fmt.Println("select best\nlen(neighbours)", len(neighbours))
for i, v := range neighbours {
switch i {
case 0:
// the first one is automatically the best.
bestSolution = f(v)
bestNeighbour = v
default:
// calculate the value of the bench function for neighbour and
// decide if this was the best we've seen so far or not.
if solution := f(v); solution < bestSolution {
bestSolution = solution
bestNeighbour = v
}
}
}
return bestNeighbour, bestSolution
}
// getBenchSearchSpaceSize calculates and returns the absolute distance between
// min,max for a given bench func.
func getBenchSearchSpaceSize(benchName string) float64 {
params := bench.FunctionParams[benchName]
return math.Abs(params.Min() - params.Max())
}
// getAllowedTweak calculates the value of allowed movement correction based on
// the bench func's searchSpaceSize, also taking into account the globally
// applicable MaxNeighbourVariance (in %) value from the bench pkg.
// the allowedTweak value should therefore the amount to at most 10% of the
// searchSpaceSize. TODO(me): floor this down.
func getAllowedTweak(searchSpaceSize float64) float64 {
// TODO(me): have this passed to HillClimb and from there into this func.
// return (bench.MaxNeighbourVariancePercent * 0.5) * (searchSpaceSize * 0.01)
return bench.MaxNeighbourVariancePercent * (searchSpaceSize * 0.01)
}
func genNeighbours(n, dimens int, benchName string, origin []float64, neighbVals []neighbour) {
if dimens < 1 {
log.Fatalf("error: neighbour count set to: %d; want len(neighbVals) > 0, bailing\n", n)
}
// create a new representation of the uniform distribution with the bounds
// unset for the moment, since we need to find out what exactly those ought
// to be (and we will - a couple of lines later).
// uniform := distuv.Uniform{Src: time.Now().UnixMicro()}
uniform := distuv.Uniform{}
// uniform.Src.Seed(uint64(time.Now().UnixNano()))
params := bench.FunctionParams[benchName]
// get bench function bounds.
benchMin := params.Min()
benchMax := params.Max()
searchSpaceSize := getBenchSearchSpaceSize(benchName)
// allowedTweak tells how big of a tweak to each side is allowed.
allowedTweak := getAllowedTweak(searchSpaceSize)
// seed the src.
uniform.Src = rand.NewSource(uint64(time.Now().UnixNano()))
for _, v := range neighbVals {
// for _, v := range neighbVals {
for i := 0; i < dimens; i++ {
newMin := origin[i] - allowedTweak
// if newMin is not within the bounds, fall back to benchMin.
if newMin < benchMin {
uniform.Min = newMin
}
// if newMax is not within the bounds, fall back to benchMax.
newMax := origin[i] + allowedTweak
if newMin > benchMax {
uniform.Max = newMax
}
// fmt.Println("newMin:", newMin, "newMax", newMax)
// v = append(v, uniform.Rand())
v[i] = uniform.Rand()
}
}
}
// singleHillClimb runs a single iteration of SHC.
func singleHillClimb(
dimens uint,
neighbCount int,
benchName string,
oldVals []float64,
) ([]float64, float64) {
neighbours := make([]neighbour, dimens)
// prealloc the slice the size of dimens (and oldVals, should be the same).
vals := make([]float64, len(oldVals))
for i := range neighbours {
neighbours[i] = vals
}
genNeighbours(neighbCount, int(dimens), benchName, oldVals, neighbours)
newVals, newRes := selectBestNeighbour(neighbours, benchName)
return newVals, newRes
}
// HillClimb performs 30 iterations of SHC (30 singleHillClimb func calls
// internally) to establish a semi-relevant statistical baseline, and reports
// the results of the computation.
// nolint: gocognit
func HillClimb(
maxFES, benchMinIters, neighbours int,
theD []int,
benchFunc string,
ch chan []stats.Stats,
) {
if maxFES <= 0 {
} else if benchMinIters <= 0 {
log.Fatalln(fmtSHCOut("benchMinIters cannot be <= 0, bailing"))
} else if _, ok := bench.Functions[benchFunc]; !ok {
log.Fatalln(fmtSHCOut(
"unknown benchFunc used: '" + benchFunc + "', bailing",
))
}
for i := range theD {
if theD[i] <= 0 {
log.Fatalln(fmtSHCOut(" no dimension in D can be <= 0, bailing"))
}
}
var (
fes int
// since Stochastic Hill Climbing performs multiple cost function
// evaluations during each run (in search of a better neighbouring
// value), fesPerIter stores how many evaluations are allowed during
// each run based on the number of allowed evaluations in total (passed
// into function in maxFES).
fesPerIter int
localD []int
minIters int
stochasticHCStats []stats.Stats
)
fes = maxFES
// no need to call math.Floor() apparently, as `int()` does that implicitly.
fesPerIter = int(float64(fes / neighbours))
localD = theD
minIters = benchMinIters
shcMeans := &stats.AlgoBenchMean{
Algo: "Stochastic Hill Climbing",
BenchMeans: make([]stats.BenchMean, 0, len(localD)),
}
for _, dimens := range localD {
stochasticHCStatDimX := &stats.Stats{
Algo: "Stochastic Hill Climbing",
Dimens: dimens,
Iterations: minIters,
// this is subject to change, see note on Stats struct in pkg
// stats.
Generations: fes,
}
funcStats := &stats.FuncStats{BenchName: benchFunc}
benchFuncParams := bench.FunctionParams[benchFunc]
dimXMean := &stats.BenchMean{
Bench: benchFunc,
Dimens: dimens,
Iterations: minIters,
Generations: fes,
Neighbours: neighbours,
}
uniDist := distuv.Uniform{
Min: benchFuncParams.Min(),
Max: benchFuncParams.Max(),
}
printSHC(fmt.Sprintf("running bench \"%s\" for %dD with %d Neighbours",
benchFunc, stochasticHCStatDimX.Dimens, neighbours),
)
funcStats.BenchResults = make([]stats.BenchRound, minIters)
// create and seed a source of preudo-randomness
src := rand.NewSource(uint64(rand.Int63()))
// src := rand.NewSource(uint64(time.Now().UnixNano()))
// track execution time.
start := time.Now()
for iter := 0; iter < minIters; iter++ {
funcStats.BenchResults[iter].Iteration = iter
// create and stochastically populate the vals slice.
initVals := make([]float64, dimens)
// reseed using current time.
uniDist.Src = src
var bestResult float64
var bestVals []float64
initialised := false
for !initialised {
// generate initial vals, make sure they are not 0s.
initValsSHC(uint(dimens), initVals, &uniDist)
if bV, bR := singleHillClimb(
uint(dimens),
neighbours,
benchFunc,
initVals,
); bR != 0 {
bestVals = bV
bestResult = bR
initialised = true
}
}
for i := 0; i < fesPerIter; i++ {
v, r := singleHillClimb(
uint(dimens),
neighbours,
benchFunc,
bestVals,
)
// first or any other iteration.
switch i {
case 0:
bestResult = r
bestVals = v
default:
if r < bestResult {
bestResult = r
bestVals = v
}
}
funcStats.BenchResults[iter].Results = append(
funcStats.BenchResults[iter].Results,
bestResult,
)
// this block makes sure we properly count func evaluations for
// the purpose of correctly comparable plot comparison. i.e.
// append the winning (current best) value neighbours-1 (the
// first best is already saved at this point) times to
// represent the fact that while evaluating the neighbours to
// the current best value is taking place in the background,
// the current best value itself is kept around and
// symbolically saved as the best of the Generation.
for x := 0; x < neighbours-1; x++ {
funcStats.BenchResults[iter].Results = append(
funcStats.BenchResults[iter].Results,
bestResult,
)
}
}
}
elapsed := time.Since(start)
printSHC("computing " + fmt.Sprint(benchMinIters) + "I of bench \"" +
benchFunc + "\" for " + fmt.Sprint(dimens) + "D took " + fmt.Sprint(elapsed),
)
// get mean vals.
dimXMean.MeanVals = stats.GetMeanVals(funcStats.BenchResults, fes)
// save to funcStats, too.
funcStats.MeanVals = dimXMean.MeanVals
stochasticHCStatDimX.BenchFuncStats = append(
stochasticHCStatDimX.BenchFuncStats,
*funcStats,
)
stochasticHCStats = append(stochasticHCStats, *stochasticHCStatDimX)
// save to AlgoMeans.
shcMeans.BenchMeans = append(shcMeans.BenchMeans, *dimXMean)
}
// log.Printf("%+v\n", shcMeans)
sort.Sort(shcMeans)
// export AlgoMeans.
mu.Lock()
meanStats.AlgoMeans = append(meanStats.AlgoMeans, *shcMeans)
mu.Unlock()
ch <- stochasticHCStats
}