Skip to content

Commit

Permalink
Incorporate stagnation and improve speciation.
Browse files Browse the repository at this point in the history
  • Loading branch information
We-Gold committed Aug 3, 2023
1 parent d82381d commit 066584f
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 93 deletions.
55 changes: 15 additions & 40 deletions examples/racing/sketch.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,29 @@ let cars
let sensorDisplay
let w = 600
let h = 400
// let training = false

const POPULATION_SIZE = 200
const POPULATION_SIZE = 100
const MAX_STEPS = 400

const FRAME_RATE = 60

const INITIAL_ANGLE = -90

let step = 0
let shortCircuit = false

const tn = TinyNEAT({
maxGenerations: 10,
maxGenerations: 15,
initialPopulationSize: POPULATION_SIZE,
inputSize: 16,
outputSize: 1,
compatibilityThreshold: 6.0,
compatibilityThreshold: 4.5,
addLinkProbability: 0.1,
addNodeProbability: 0.2,
mutateWeightProbability: 0.4,
interspeciesMatingRate: 0.01,
largeNetworkSize: 30,
maximumStagnation: 5,
nnPlugin: plugins.ANNPlugin({ activation: "posAndNegSigmoid" }),
})

Expand Down Expand Up @@ -56,8 +59,9 @@ function draw(p) {

p.loadPixels()

if (++step % MAX_STEPS === 0) {
if (++step % MAX_STEPS === 0 || shortCircuit) {
step = 0
shortCircuit = false

tn.evolve()

Expand All @@ -72,9 +76,12 @@ function draw(p) {

const population = tn.getPopulation()

let carsOffTrack = 0

for (const [i, car] of cars.entries()) {
// Skip any cars that leave the track fully
if (car.isOffTrack()) {
carsOffTrack++
continue
}

Expand All @@ -85,49 +92,17 @@ function draw(p) {

population[i].fitness += car.receiveOutput(outputs[0])

// Render every 20th car
if (i % 20 === 0) {
car.render()
}
car.render()
}

if (carsOffTrack === cars.length) shortCircuit = true

// Things to draw on top of the map and car
sensorDisplay.showSensors(cars[0].sensors)

p.textSize(16)
p.fill(255)
// p.text(`Generation: ${0}`, w - 130, 20)
p.text(`FPS: ${Math.round(p.frameRate())}`, w - 130, 20)
// textSize(15)
// fill(255, 0, 0)
// text("Speed: " + car.vel, w - 70, 20)

// if (!training && !car.modelTrained) {
// textSize(15)
// fill(255, 0, 0)
// text("AI Status: Gathering Data", w - 180, 40)
// }

// if (training && !car.modelTrained) {
// textSize(15)
// fill(255, 0, 0)
// text("AI Status: Training", w - 130, 40)
// }

// if (training && car.modelTrained) {
// textSize(15)
// fill(255, 0, 0)
// text("AI Status: Active", w - 112, 40)
// }

// if (
// !training &&
// frameCount % 30 == 0 &&
// car.memory.mem.length == car.memory.memSize
// ) {
// training = true
// car.initModel()
// }
}

const sketch = p => {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "tinyneat",
"author": "Weaver Goldman <[email protected]>",
"description": "TinyNEAT is a tiny, js-native NEAT (NeuroEvolution of Augmenting Topologies) library, meant to easily incorporate into any codebase.",
"version": "1.0.1",
"version": "1.1.0",
"type": "module",
"license": "MIT",
"repository": {
Expand Down
65 changes: 47 additions & 18 deletions src/evolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,50 @@ import {
import { chooseRandom, random } from "./helpers"
import { InnovationHistory } from "./history"
import { createAdjacencyList, topologicalSort } from "./nn/nnplugin"
import { speciatePopulation } from "./species"
import { Species, speciatePopulation } from "./species"

export const evolvePopulation = (
population: Genome[],
species: Genome[][],
previousSpecies: Species[],
innovationHistory: InnovationHistory,
config: Config,
generation: number,
) => {
// Separate the current population into species
speciatePopulation(population, species, config)
const nextSpecies = speciatePopulation(
population,
previousSpecies,
config,
generation,
)

// Adjust the compatibility threshold based on the number of species
adjustCompatibilityThreshold(species, config)
adjustCompatibilityThreshold(nextSpecies, config)

// Adjust the fitness of each organism to normalize based on species size
calculateAdjustedFitnesses(species)
calculateAdjustedFitnesses(nextSpecies)

// Allocate the appropriate number of offspring for each species
const offspringAllocation = allocateOffspring(population, species)
const offspringAllocation = allocateOffspring(
population,
nextSpecies,
config,
generation,
)

const nextPopulation = []

for (const [i, s] of species.entries()) {
for (const [i, s] of nextSpecies.entries()) {
// Sort the parents in descending order by fitness
const sortedParents = s.sort(
const sortedParents = s.population.sort(
(a, b) => b.adjustedFitness - a.adjustedFitness, // This currently assumes positive fitness is ideal
)

// Choose the top performing species individuals to become parents
const viableParents = sortedParents.slice(
0,
Math.max(
s.length * config.survivalThreshold,
s.population.length * config.survivalThreshold,
config.minimumSpeciesSize,
),
)
Expand Down Expand Up @@ -70,7 +81,7 @@ export const evolvePopulation = (

// In rare cases, allow interspecies crossover
if (random(config.interspeciesMatingRate)) {
parent2 = chooseRandom(chooseRandom(species))
parent2 = chooseRandom(chooseRandom(nextSpecies).population)
}

let childGenes: ConnectionGene[]
Expand Down Expand Up @@ -134,22 +145,30 @@ export const evolvePopulation = (
}
}

return nextPopulation
return { nextPopulation, nextSpecies }
}

const calculateAdjustedFitnesses = (species: Genome[][]) => {
const calculateAdjustedFitnesses = (species: Species[]) => {
// Normalize the fitness of each species by the species size
for (const s of species) {
for (const genome of s) {
genome.adjustedFitness = genome.fitness / s.length
for (const genome of s.population) {
genome.adjustedFitness = genome.fitness / s.population.length
}
}
}

const allocateOffspring = (population: Genome[], species: Genome[][]) => {
const allocateOffspring = (
population: Genome[],
species: Species[],
config: Config,
generation: number,
) => {
// Calculate the average fitness of each species
const speciesFitnessAverages = species.map(s => {
return s.reduce((acc, curr) => acc + curr.adjustedFitness, 0) / s.length
return (
s.population.reduce((acc, curr) => acc + curr.adjustedFitness, 0) /
s.population.length
)
})

// Sum all of the species average fitnesses
Expand All @@ -159,12 +178,22 @@ const allocateOffspring = (population: Genome[], species: Genome[][]) => {
)

// Calculate the proportion of the population to allocate to each species
return speciesFitnessAverages.map(averageFitness =>
const allocatedOffspring = speciesFitnessAverages.map(averageFitness =>
Math.round((averageFitness / totalAverageFitness) * population.length),
)

// If the species hasn't improved its fitness in a certain number of generations,
// half the number of offspring it is given
return allocatedOffspring.map((offspring, index) => {
const speciesIsStagnant =
generation - species[index].recordGeneration >=
config.maximumStagnation

return speciesIsStagnant ? Math.floor(offspring / 2) : offspring
})
}

const adjustCompatibilityThreshold = (species: Genome[][], config: Config) => {
const adjustCompatibilityThreshold = (species: Species[], config: Config) => {
// Modify the compatibility threshold to dynamically control the number of species
if (species.length < config.targetSpecies) {
config.compatibilityThreshold -= config.compatibilityModifier
Expand Down
10 changes: 8 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Genome, createEmptyGenome } from "./genome"
import { createHallOfFame } from "./halloffame"
import { createInnovationHistory } from "./history"
import * as plugins from "./plugins"
import { Species } from "./species"

/**
* Creates a NEAT interface using the given configuration.
Expand All @@ -22,7 +23,7 @@ const TinyNEAT = (partialConfig: PartialConfig = {}) => {
let population: Genome[] = Array(config.initialPopulationSize)

// Create a list for storing species
const species: Genome[][] = []
let species: Species[] = []

// Create a list to store innovations (key is the pair of nodes)
const innovationHistory = createInnovationHistory()
Expand Down Expand Up @@ -62,13 +63,18 @@ const TinyNEAT = (partialConfig: PartialConfig = {}) => {
// Update the hall of fame
population.forEach(genome => hallOfFame.tryAdding(genome))

population = evolvePopulation(
const { nextPopulation, nextSpecies } = evolvePopulation(
population,
species,
innovationHistory,
config,
generation,
)

// Update the population and species
population = nextPopulation
species = nextSpecies

generation++

// Dispatch the evolve event
Expand Down
3 changes: 2 additions & 1 deletion src/logging/loggingmanager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Config } from "../config"
import { Genome } from "../genome"
import { Species } from "../species"

export type InitialPopulationData = {
population: readonly Genome[]
Expand All @@ -9,7 +10,7 @@ export type InitialPopulationData = {
export type EvolveData = {
generation: number
population: readonly Genome[]
species: readonly Genome[][]
species: readonly Species[]
bestGenomes: readonly Genome[]
config: Config
complete: boolean
Expand Down
Loading

0 comments on commit 066584f

Please sign in to comment.