identify an especially tricky bug

This commit is contained in:
Michael Peters 2024-09-03 21:41:54 -07:00
parent 9e624f225e
commit c7abcbb03a
4 changed files with 78 additions and 27 deletions

View File

@ -25,10 +25,10 @@ const FEATURES = {
HEAD_Y: 'HEAD_Y', HEAD_Y: 'HEAD_Y',
APPLE_REL_X: 'APPLE_REL_X', APPLE_REL_X: 'APPLE_REL_X',
APPLE_REL_Y: 'APPLE_REL_Y', APPLE_REL_Y: 'APPLE_REL_Y',
TAIL_ABOVE: 'TAIL_ABOVE', // TAIL_ABOVE: 'TAIL_ABOVE',
TAIL_BELOW: 'TAIL_BELOW', // TAIL_BELOW: 'TAIL_BELOW',
TAIL_LEFT: 'TAIL_LEFT', // TAIL_LEFT: 'TAIL_LEFT',
TAIL_RIGHT: 'TAIL_RIGHT', // TAIL_RIGHT: 'TAIL_RIGHT',
}; };
const OUTPUTS = { const OUTPUTS = {
U: 'U', U: 'U',
@ -43,10 +43,10 @@ const _BASE_GENOME_SNAKE_BRAIN_NEAT_EDGES = completeBipartiteEdges(
FEATURES.HEAD_Y, FEATURES.HEAD_Y,
FEATURES.APPLE_REL_X, FEATURES.APPLE_REL_X,
FEATURES.APPLE_REL_Y, FEATURES.APPLE_REL_Y,
FEATURES.TAIL_ABOVE, // FEATURES.TAIL_ABOVE,
FEATURES.TAIL_BELOW, // FEATURES.TAIL_BELOW,
FEATURES.TAIL_LEFT, // FEATURES.TAIL_LEFT,
FEATURES.TAIL_RIGHT, // FEATURES.TAIL_RIGHT,
], ],
[OUTPUTS.U, OUTPUTS.D, OUTPUTS.L, OUTPUTS.R], [OUTPUTS.U, OUTPUTS.D, OUTPUTS.L, OUTPUTS.R],
); );
@ -108,10 +108,10 @@ export class NEATSnakeBrain {
act.set(FEATURES.HEAD_Y, head.y); act.set(FEATURES.HEAD_Y, head.y);
act.set(FEATURES.APPLE_REL_X, appleRel.x); act.set(FEATURES.APPLE_REL_X, appleRel.x);
act.set(FEATURES.APPLE_REL_Y, appleRel.y); act.set(FEATURES.APPLE_REL_Y, appleRel.y);
act.set(FEATURES.TAIL_ABOVE, Math.max(...above, BOARD_SQUARES)); // act.set(FEATURES.TAIL_ABOVE, Math.max(...above, BOARD_SQUARES));
act.set(FEATURES.TAIL_BELOW, Math.max(...below, BOARD_SQUARES)); // act.set(FEATURES.TAIL_BELOW, Math.max(...below, BOARD_SQUARES));
act.set(FEATURES.TAIL_LEFT, Math.max(...left, BOARD_SQUARES)); // act.set(FEATURES.TAIL_LEFT, Math.max(...left, BOARD_SQUARES));
act.set(FEATURES.TAIL_RIGHT, Math.max(...right, BOARD_SQUARES)); // act.set(FEATURES.TAIL_RIGHT, Math.max(...right, BOARD_SQUARES));
this.brain.think(act); this.brain.think(act);

View File

@ -228,7 +228,7 @@
* - Effectively pass data from inputs, through hidden nodes, to outputs * - Effectively pass data from inputs, through hidden nodes, to outputs
*/ */
import { edgesToNodes, NodeID, traceParents, RawEdge } from './network'; import { edgesToNodes, NodeID, traceParents, RawEdge, traceChildren } from './network';
import { keyMax, mapInvert, mapMap, randchoice, randint, randomNegPos, setDifference, setMap } from './util'; import { keyMax, mapInvert, mapMap, randchoice, randint, randomNegPos, setDifference, setMap } from './util';
export interface GeneData { export interface GeneData {
@ -258,6 +258,33 @@ let g_node_id = 1;
let g_innovation_number = 1; let g_innovation_number = 1;
let g_species_id = 1; let g_species_id = 1;
function hashGenome(genome: Genome) {
const cyrb53 = (str: string, seed = 0) => {
let h1 = 0xdeadbeef ^ seed;
let h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
};
let hash = 0;
for (const gene of genome) {
hash += cyrb53(gene.src_id);
hash += cyrb53(gene.dst_id);
hash += gene.data.weight;
hash += gene.data.enabled ? 1 : 0;
hash += gene.data.innovation;
}
return hash;
}
export function resetGlobalIDs(config: { node_id: number; innovation_number: number; species_id: number }) { export function resetGlobalIDs(config: { node_id: number; innovation_number: number; species_id: number }) {
g_node_id = config.node_id; g_node_id = config.node_id;
g_innovation_number = config.innovation_number; g_innovation_number = config.innovation_number;
@ -508,7 +535,8 @@ export function mutateNewConn(newConn: { src_id: NodeID; dst_id: NodeID }, newWe
}; };
} }
export function findAcyclicNewConns<DataT>(rawEdges: RawEdge<DataT>[]): { src_id: NodeID; dst_id: NodeID }[] { // TODO: improve name since also excluding source nodes
export function findAcyclicNonSourceNewConns<DataT>(rawEdges: RawEdge<DataT>[]): { src_id: NodeID; dst_id: NodeID }[] {
// finds potential new connections that are acyclic and are not already connected // finds potential new connections that are acyclic and are not already connected
// NOTE: there's some performance stuff here that could definitely be improved // NOTE: there's some performance stuff here that could definitely be improved
@ -518,12 +546,22 @@ export function findAcyclicNewConns<DataT>(rawEdges: RawEdge<DataT>[]): { src_id
// a node that is connected to one of its parents creates a cycle // a node that is connected to one of its parents creates a cycle
const allNodeIDs = new Set(nodes.keys()); const allNodeIDs = new Set(nodes.keys());
const parents = traceParents(nodes); const parents = traceParents(nodes);
const children = traceChildren(nodes);
// TODO: think on this more:
// adding a new connection needs to not connect a source -> source or a sink -> sink
// or else the innovation number system gets messed up
const sources = new Set(Array.from(parents.entries()).flatMap(([k, v]) => (v.size === 0 ? [k] : [])));
const sinks = new Set(Array.from(children.entries()).flatMap(([k, v]) => (v.size === 0 ? [k] : [])));
console.log({ sources, sinks });
const acyclic = new Map<NodeID, Set<NodeID>>(); const acyclic = new Map<NodeID, Set<NodeID>>();
for (const [nodeID, nodeParents] of parents.entries()) { for (const [nodeID, nodeParents] of parents.entries()) {
const nodeParentIDs = setMap(nodeParents, n => n.id); const nodeParentIDs = setMap(nodeParents, n => n.id);
const nodeIDsAcyclic = setDifference(allNodeIDs, nodeParentIDs); const nodeIDsAcyclic = setDifference(allNodeIDs, nodeParentIDs);
nodeIDsAcyclic.delete(nodeID); nodeIDsAcyclic.delete(nodeID);
acyclic.set(nodeID, nodeIDsAcyclic); const nodeIDsAcyclicNonSource = setDifference(nodeIDsAcyclic, sources);
acyclic.set(nodeID, nodeIDsAcyclicNonSource);
} }
// flatten // flatten
@ -552,6 +590,9 @@ export interface MutateConfig {
export function mutate(genome: Genome, config: MutateConfig): Genome { export function mutate(genome: Genome, config: MutateConfig): Genome {
const { mutate_rate, assign_rate, assign_mag, perturb_mag, new_node_rate, new_connection_rate } = config; const { mutate_rate, assign_rate, assign_mag, perturb_mag, new_node_rate, new_connection_rate } = config;
console.log('mutating: ', hashGenome(genome), genome);
findAcyclicNonSourceNewConns(genome);
const newGenome = genome.map(gene => { const newGenome = genome.map(gene => {
if (Math.random() >= mutate_rate) return gene; // this connection should not be mutated if (Math.random() >= mutate_rate) return gene; // this connection should not be mutated
if (Math.random() < assign_rate) { if (Math.random() < assign_rate) {
@ -574,8 +615,10 @@ export function mutate(genome: Genome, config: MutateConfig): Genome {
} }
if (Math.random() < new_connection_rate) { if (Math.random() < new_connection_rate) {
// TODO: figure out why we're not hitting a
// TODO: disallow "source" nodes from being new connection destinations
// create a new connection between two *previously unconnected* nodes // create a new connection between two *previously unconnected* nodes
const options = findAcyclicNewConns(newGenome); const options = findAcyclicNonSourceNewConns(newGenome);
if (options.length === 0) { if (options.length === 0) {
// TODO: remove this warn once this starts working // TODO: remove this warn once this starts working
// this is mostly a sanity check / useful for metrics // this is mostly a sanity check / useful for metrics
@ -583,10 +626,17 @@ export function mutate(genome: Genome, config: MutateConfig): Genome {
} else { } else {
// choose a random connection // choose a random connection
const newConn = randchoice(options); const newConn = randchoice(options);
console.log('adding new connection...', hashGenome(genome), genome, newConn);
if (new Set(['U', 'D', 'L', 'R']).has(newConn.dst_id)) {
debugger;
}
const newGene = mutateNewConn(newConn, assign_mag * randomNegPos()); const newGene = mutateNewConn(newConn, assign_mag * randomNegPos());
newGenome.push(newGene); newGenome.push(newGene);
} }
findAcyclicNonSourceNewConns(newGenome);
} }
console.log('created: ', hashGenome(newGenome), newGenome);
return newGenome; return newGenome;
} }
@ -619,6 +669,7 @@ export function computeNextGeneration(
const mom = randchoice(winners); const mom = randchoice(winners);
const dad = chooseMate(mom, winnersPopulation, mcc); const dad = chooseMate(mom, winnersPopulation, mcc);
const crossed = crossGenomes(mom, dad, fitness, cc); const crossed = crossGenomes(mom, dad, fitness, cc);
// TODO: crossed has cycles!
const baby = mutate(crossed, mc); const baby = mutate(crossed, mc);
// assign to a species + add to next generation // assign to a species + add to next generation

View File

@ -80,7 +80,7 @@ export function traceParents<DataT>(nodes: Network<Node<DataT>>) {
function traceNodeParents(node: Node<DataT>, depth: number): Set<Node<DataT>> { function traceNodeParents(node: Node<DataT>, depth: number): Set<Node<DataT>> {
console.log('tracing: ', node.id, depth); console.log('tracing: ', node.id, depth);
// TODO: there's a particularly nasty issue where maybe nodes are not just from their own genome? // TODO: there's a particularly nasty issue where maybe nodes are not just from their own genome?
if (depth > 1000) { if (depth > 20) {
debugger; debugger;
} }
if (parents.has(node.id)) return parents.get(node.id)!; if (parents.has(node.id)) return parents.get(node.id)!;

View File

@ -4,7 +4,7 @@ import {
tournamentSelectionWithChampions, tournamentSelectionWithChampions,
compatibilityDistance, compatibilityDistance,
crossGenomes, crossGenomes,
findAcyclicNewConns, findAcyclicNonSourceNewConns,
Genome, Genome,
mutateAssign, mutateAssign,
mutateNewConn, mutateNewConn,
@ -357,7 +357,7 @@ function testMutateNewConn() {
} }
addTest(testMutateNewConn); addTest(testMutateNewConn);
function testFindAcyclicNewConns() { function testFindAcyclicNonSourceNewConns() {
/* /*
* all edges pointing down * all edges pointing down
* *
@ -377,22 +377,22 @@ function testFindAcyclicNewConns() {
{ src_id: 'D', dst_id: 'F', data: null }, { src_id: 'D', dst_id: 'F', data: null },
]; ];
const options = findAcyclicNewConns(edges); const options = findAcyclicNonSourceNewConns(edges);
const expected = [ const expected = [
{ src_id: 'A', dst_id: 'B' }, // not A -> B - B is a source
{ src_id: 'A', dst_id: 'F' }, { src_id: 'A', dst_id: 'F' },
{ src_id: 'B', dst_id: 'A' }, // not B -> A - A is a source
{ src_id: 'B', dst_id: 'C' }, { src_id: 'B', dst_id: 'C' },
{ src_id: 'B', dst_id: 'E' }, { src_id: 'B', dst_id: 'E' },
{ src_id: 'B', dst_id: 'F' }, { src_id: 'B', dst_id: 'F' },
{ src_id: 'C', dst_id: 'B' }, // not C -> B - B is a source
{ src_id: 'C', dst_id: 'D' }, { src_id: 'C', dst_id: 'D' },
{ src_id: 'C', dst_id: 'F' }, { src_id: 'C', dst_id: 'F' },
{ src_id: 'D', dst_id: 'C' }, { src_id: 'D', dst_id: 'C' },
// not E -> B // not E -> B - B is a parent of E
{ src_id: 'E', dst_id: 'F' }, { src_id: 'E', dst_id: 'F' },
// not F -> A // not F -> A - A is a parent of F
// not F -> B // not F -> B - B is a parent of F
{ src_id: 'F', dst_id: 'C' }, { src_id: 'F', dst_id: 'C' },
{ src_id: 'F', dst_id: 'E' }, { src_id: 'F', dst_id: 'E' },
]; ];
@ -406,7 +406,7 @@ function testFindAcyclicNewConns() {
assertDeepEqual(options, expected); assertDeepEqual(options, expected);
} }
addTest(testFindAcyclicNewConns); addTest(testFindAcyclicNonSourceNewConns);
function testComputeNextGeneration() { function testComputeNextGeneration() {
function makeGenome(weight: number) { function makeGenome(weight: number) {