Skip to content

Commit

Permalink
Speciation WIP (#368)
Browse files Browse the repository at this point in the history
* Refactor import paths in DeDuplicator.ts, CreatureUUID.ts, Breed.ts, ExperimentStore.ts, and Creature.ts

* Refactor sortCreaturesByScore function and add verbose logging in ElitismUtils.ts

* Refactor checkAndAdd function in Neat.ts to improve readability and fix error handling

* Refactor checkAndAdd function in Neat.ts to improve readability and error handling

* Refactor Genus class to add findClosestMatchingSpecies method

* Refactor Genus class to add findClosestMatchingSpecies method

* Fix error handling in Creature.ts and CreatureState.ts

* Fix bias range check in Propagate/STEP.ts and add missing synapses in FineTune.ts

---------

Co-authored-by: Nigel Leck <[email protected]>
  • Loading branch information
nleck and nigelleck authored May 5, 2024
1 parent f17b819 commit af0d4cc
Show file tree
Hide file tree
Showing 19 changed files with 829 additions and 153 deletions.
23 changes: 14 additions & 9 deletions src/Creature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
} from "./architecture/CreatureState.ts";
import { creatureValidate } from "./architecture/CreatureValidate.ts";
import { DataRecordInterface, makeDataDir } from "./architecture/DataSet.ts";
import { Neat } from "./architecture/Neat.ts";
import { Neat } from "./NEAT/Neat.ts";
import { Neuron } from "./architecture/Neuron.ts";
import {
NeuronExport,
Expand Down Expand Up @@ -734,16 +734,17 @@ export class Creature implements CreatureInternal {
options.log &&
(generation % options.log === 0 || completed)
) {
let avgTxt = "";
if (Number.isFinite(result.averageScore)) {
avgTxt = ` (avg: ${yellow(result.averageScore.toFixed(4))})`;
}
console.log(
"Generation",
generation,
"score",
fittest.score,
" (avg:",
yellow(
result.averageScore.toFixed(4),
),
") error",
avgTxt,
"error",
error,
(options.log > 1 ? "avg " : "") + "time",
yellow(
Expand Down Expand Up @@ -849,9 +850,13 @@ export class Creature implements CreatureInternal {
for (let i = files.length; i--;) {
const json = JSON.parse(Deno.readTextFileSync(files[i]));

const result = this.evaluateData(json, cost, feedbackLoop);
totalCount += result.count;
totalError += result.error;
try {
const result = this.evaluateData(json, cost, feedbackLoop);
totalCount += result.count;
totalError += result.error;
} catch (e) {
throw new Error(`Error in file: ${files[i]}`, e);
}
}
return { error: totalError / totalCount };
}
Expand Down
182 changes: 182 additions & 0 deletions src/NEAT/FineTunePopulation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { assert } from "https://deno.land/[email protected]/assert/mod.ts";
import { Creature, CreatureUtil } from "../../mod.ts";
import { Genus } from "./Genus.ts";
import { fineTuneImprovement } from "../architecture/FineTune.ts";
import { Species } from "./Species.ts";
import { Neat } from "./Neat.ts";

export class FindTunePopulation {
private neat: Neat;
constructor(neat: Neat) {
this.neat = neat;
}

async make(
fittest: Creature,
previousFittest: Creature | undefined,
genus: Genus,
) {
assert(fittest, "Fittest creature mandatory");
const fittestUUID = await CreatureUtil.makeUUID(fittest);

const uniqueUUID = new Set<string>([fittestUUID]);

const tmpFineTunePopulation = [];

// Add previousFittest first if it's different from fittest and not null
if (
previousFittest
) {
const previousUUID = await CreatureUtil.makeUUID(previousFittest);
if (!uniqueUUID.has(previousUUID)) {
tmpFineTunePopulation.push(previousFittest);
uniqueUUID.add(previousUUID);
}
}

// Add remaining creatures from the population excluding fittest and previousFittest
for (const creature of this.neat.population) {
const creatureUUID = await CreatureUtil.makeUUID(creature);
if (!uniqueUUID.has(creatureUUID)) {
tmpFineTunePopulation.push(creature);
uniqueUUID.add(creatureUUID);
}
}

const tmpPreviousFittest = tmpFineTunePopulation.shift();

let fineTunedPopulation: Creature[] = [];
if (!tmpPreviousFittest) {
console.warn("Failed to find previous fittest creature");
} else {
/** 20% of population or those that just died, leave one for the extended */
const fineTunePopSize = Math.max(
Math.ceil(
this.neat.config.populationSize / 5,
),
this.neat.config.populationSize - this.neat.population.length -
this.neat.config.elitism -
this.neat.trainingComplete.length,
);

const tunedUUID = new Set<string>();

tunedUUID.add(fittestUUID);

tunedUUID.add(tmpPreviousFittest.uuid ?? "UNKNOWN");
fineTunedPopulation = await fineTuneImprovement(
fittest,
tmpPreviousFittest,
fineTunePopSize - 1,
this.neat.config.verbose,
);

for (let attempts = 0; attempts < 12; attempts++) {
/**
* Now, after we do the fine tuning of the fittest versus the previous fittest,
* I want to find another creature from the same species of the fittest creature ( but not the fittest or previous fittest creatures)
* and perform the fine tuning comparing the fittest creature to another within the species.
*
* We should favor the highest score creatures in that species.
*/

const speciesFineTunePopSize = fineTunePopSize -
fineTunedPopulation.length;

if (speciesFineTunePopSize < 1) break;

const speciesKey = await Species.calculateKey(fittest);
const species = genus.speciesMap.get(speciesKey);

if (species) {
if (species.creatures.length > 0) { // Ensure there's more than one to choose from
let eligibleCreatures = species.creatures.filter((creature) =>
!tunedUUID.has(creature.uuid ?? "UNKNOWN")
);

/** If there is no eligible creatures try find the closest species. */
if (eligibleCreatures.length == 0) {
const closestSpecies = genus.findClosestMatchingSpecies(fittest);
if (closestSpecies) {
if (closestSpecies && closestSpecies.creatures.length > 0) {
eligibleCreatures = closestSpecies.creatures.filter(
(creature) => !tunedUUID.has(creature.uuid ?? "UNKNOWN"),
);
}
}
}

if (eligibleCreatures.length > 0) {
// Introduce random selection, weighted towards higher score creatures
const nextBestCreature = this.weightedRandomSelect(
eligibleCreatures,
);

tunedUUID.add(nextBestCreature.uuid ?? "UNKNOWN");
const extendedTunedPopulation = await fineTuneImprovement(
fittest,
nextBestCreature,
speciesFineTunePopSize,
false,
);

fineTunedPopulation.push(...extendedTunedPopulation);
}
}
} else {
throw new Error(`No species found for key ${speciesKey}`);
}

const extendedFineTunePopSize = fineTunePopSize -
fineTunedPopulation.length;
if (extendedFineTunePopSize > 0 && tmpFineTunePopulation.length > 0) {
/* Choose a creature from near the top of the list. */
const location = Math.floor(
tmpFineTunePopulation.length * Math.random() * Math.random(),
);

const extendedPreviousFittest = tmpFineTunePopulation[location];
if (!extendedPreviousFittest) {
throw new Error(
`No creature found at location ${location} in tmpFineTunePopulation.`,
);
}
tunedUUID.add(extendedPreviousFittest.uuid ?? "UNKNOWN");
const extendedTunedPopulation = await fineTuneImprovement(
fittest,
extendedPreviousFittest,
extendedFineTunePopSize,
false,
);

fineTunedPopulation.push(...extendedTunedPopulation);

/* Remove the chosen creature from the array */
tmpFineTunePopulation.splice(location, 1);
} else {
break;
}
}
}

return fineTunedPopulation;
}

/* Assuming weightedRandomSelect selects based on score, weighting higher scores more heavily.*/

weightedRandomSelect(creatures: Creature[]) {
const totalWeight = creatures.reduce(
(sum, creature) => sum + 1 / (creatures.indexOf(creature) + 1),
0,
);
let random = Math.random() * totalWeight;

for (const creature of creatures) {
random -= 1 / (creatures.indexOf(creature) + 1);
if (random <= 0) {
return creature;
}
}
return creatures[0]; // Fallback to the first creature if no selection occurs
}
}
77 changes: 77 additions & 0 deletions src/NEAT/Genus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { assert } from "https://deno.land/[email protected]/assert/mod.ts";
import { Creature } from "../../mod.ts";
import { Species } from "./Species.ts";

export class Genus {
readonly speciesMap: Map<string, Species>;
readonly creatureToSpeciesMap: Map<string, string>;

constructor() {
this.speciesMap = new Map();
this.creatureToSpeciesMap = new Map();
}

async addCreature(creature: Creature): Promise<Species> {
if (creature === undefined || creature.uuid === undefined) {
throw new Error(`creature ${creature.uuid} is undefined`);
}

const key = await Species.calculateKey(creature);

let species = this.speciesMap.get(key);
if (!species) {
species = new Species(key);
this.speciesMap.set(key, species);
}
species.addCreature(creature);

this.creatureToSpeciesMap.set(creature.uuid, key);

return species;
}

findSpeciesByCreatureUUID(uuid: string): Species {
const speciesKey = this.creatureToSpeciesMap.get(uuid);

if (!speciesKey) {
throw new Error(`Could not find species for creature ${uuid}`);
}
const species = this.speciesMap.get(speciesKey);

if (!species) {
throw new Error(`Could not find species ${speciesKey}`);
}

return species;
}

findClosestMatchingSpecies(creature: Creature): Species | null {
assert(creature.uuid, "creature.uuid is undefined");
const creatureSpeciesKey = this.creatureToSpeciesMap.get(creature.uuid);
const creatureNeuronCount = creature.neurons.length;
let closestSpecies: Species | null = null;
let smallestDifference = Infinity;
let largestPopulation = 0;

this.speciesMap.forEach((species, key) => {
if (key === creatureSpeciesKey) return; // Skip the creature's current species
const exampleCreature = species.creatures[0]; // Assume at least one creature per species
const difference = Math.abs(
creatureNeuronCount - exampleCreature.neurons.length,
);

// Check if this species is closer or if it's equally close but more populated
if (
difference < smallestDifference ||
(difference === smallestDifference &&
species.creatures.length > largestPopulation)
) {
closestSpecies = species;
smallestDifference = difference;
largestPopulation = species.creatures.length;
}
});

return closestSpecies;
}
}
Loading

0 comments on commit af0d4cc

Please sign in to comment.