prototype: GA evolution spike -- sin(x) prediction validates pipeline (#66)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
## ga_spike.nim — throwaway GA neuroevolution spike: evolve ANN to predict sin(x)
|
||||
## Validates: population init, fitness eval, truncation selection, gaussian
|
||||
## mutation on all weights, single-elite preservation, champion tracking.
|
||||
## Issue #66.
|
||||
|
||||
import std/[math, random, algorithm, strformat]
|
||||
|
||||
const
|
||||
InputDim = 10 # sliding window of 10 past sin values
|
||||
HiddenDim = 4
|
||||
OutputDim = 1
|
||||
NumWeights = (InputDim * HiddenDim + HiddenDim) + # W1 + b1
|
||||
(HiddenDim * OutputDim + OutputDim) # W2 + b2 = 49
|
||||
PopSize = 200
|
||||
Generations = 200
|
||||
TopFrac = 0.2 # keep top 20%
|
||||
Sigma = 0.01 # gaussian mutation sigma on ALL weights
|
||||
EvalPoints = 50 # fitness eval sample size
|
||||
|
||||
type Individual = object
|
||||
weights: seq[float64]
|
||||
fitness: float64
|
||||
|
||||
proc forward(w: seq[float64]; input: array[InputDim, float64]): float64 =
|
||||
## 10->4->1 tanh ANN, flat weight layout: W1[40], b1[4], W2[4], b2[1]
|
||||
var hidden: array[HiddenDim, float64]
|
||||
for h in 0..<HiddenDim:
|
||||
var s = w[InputDim * HiddenDim + h] # b1[h]
|
||||
for i in 0..<InputDim:
|
||||
s += w[h * InputDim + i] * input[i] # W1[h,i]
|
||||
hidden[h] = tanh(s)
|
||||
var s = w[InputDim * HiddenDim + HiddenDim + HiddenDim] # b2[0]
|
||||
for h in 0..<HiddenDim:
|
||||
s += w[InputDim * HiddenDim + HiddenDim + h] * hidden[h] # W2[h]
|
||||
result = tanh(s)
|
||||
|
||||
proc evaluate(ind: var Individual; rng: var Rand) =
|
||||
## Fitness = -MSE on EvalPoints random samples of sin prediction.
|
||||
var mse = 0.0
|
||||
for _ in 0..<EvalPoints:
|
||||
let x = rng.rand(0.0 .. 20.0 * PI)
|
||||
var input: array[InputDim, float64]
|
||||
for i in 0..<InputDim:
|
||||
input[i] = sin(x - float64(InputDim - 1 - i))
|
||||
let target = sin(x)
|
||||
let pred = forward(ind.weights, input)
|
||||
mse += (pred - target) * (pred - target)
|
||||
ind.fitness = -mse / EvalPoints.float64
|
||||
|
||||
proc mutate(ind: var Individual; rng: var Rand) =
|
||||
## Additive gaussian on ALL weights, per GA-parameters research.
|
||||
for i in 0..<ind.weights.len:
|
||||
ind.weights[i] += rng.gauss(0.0, Sigma)
|
||||
|
||||
when isMainModule:
|
||||
var rng = initRand(42)
|
||||
|
||||
# Init population: random weights in [-0.5, 0.5]
|
||||
var pop = newSeq[Individual](PopSize)
|
||||
for i in 0..<PopSize:
|
||||
pop[i].weights = newSeq[float64](NumWeights)
|
||||
for j in 0..<NumWeights:
|
||||
pop[i].weights[j] = rng.rand(-0.5 .. 0.5)
|
||||
|
||||
echo &"GA spike: {PopSize} individuals, {NumWeights} weights, {Generations} gens, sigma={Sigma}"
|
||||
|
||||
for gen in 0..<Generations:
|
||||
# Evaluate
|
||||
for i in 0..<PopSize:
|
||||
pop[i].evaluate(rng)
|
||||
|
||||
# Sort descending by fitness (higher = better)
|
||||
pop.sort(proc(a, b: Individual): int = cmp(b.fitness, a.fitness))
|
||||
|
||||
if gen mod 20 == 0 or gen == Generations - 1:
|
||||
echo &" gen {gen:3d} best_fitness={pop[0].fitness:.6f} (MSE={-pop[0].fitness:.6f})"
|
||||
|
||||
# Selection: keep top 20%
|
||||
let nParents = max(1, int(PopSize.float64 * TopFrac))
|
||||
|
||||
# Next generation: elite(1) + mutated children from parents
|
||||
var next = newSeq[Individual](PopSize)
|
||||
next[0] = pop[0] # single elite, unchanged
|
||||
for i in 1..<PopSize:
|
||||
let parent = rng.rand(0..<nParents)
|
||||
next[i].weights = pop[parent].weights # clone parent
|
||||
next[i].mutate(rng)
|
||||
pop = next
|
||||
|
||||
# Final evaluation of champion
|
||||
pop[0].evaluate(rng)
|
||||
echo &"\nChampion fitness: {pop[0].fitness:.6f} (MSE={-pop[0].fitness:.6f})"
|
||||
|
||||
# Print predictions vs actual
|
||||
echo "\nPredictions (sample):"
|
||||
var totalErr = 0.0
|
||||
let nSamples = 20
|
||||
for i in 0..<nSamples:
|
||||
let x = float64(i) * 1.0
|
||||
var input: array[InputDim, float64]
|
||||
for j in 0..<InputDim:
|
||||
input[j] = sin(x - float64(InputDim - 1 - j))
|
||||
let target = sin(x)
|
||||
let pred = forward(pop[0].weights, input)
|
||||
totalErr += abs(pred - target)
|
||||
echo &" x={x:5.1f} sin(x)={target:+.4f} pred={pred:+.4f} err={abs(pred-target):.4f}"
|
||||
|
||||
let avgErr = totalErr / nSamples.float64
|
||||
echo &"\nAverage absolute error: {avgErr:.4f}"
|
||||
assert avgErr < 0.1, &"Champion avg error {avgErr:.4f} >= 0.1 — evolution did not converge"
|
||||
echo "PASS: assert avgErr < 0.1"
|
||||
Reference in New Issue
Block a user