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',
APPLE_REL_X: 'APPLE_REL_X',
APPLE_REL_Y: 'APPLE_REL_Y',
TAIL_ABOVE: 'TAIL_ABOVE',
TAIL_BELOW: 'TAIL_BELOW',
TAIL_LEFT: 'TAIL_LEFT',
TAIL_RIGHT: 'TAIL_RIGHT',
// TAIL_ABOVE: 'TAIL_ABOVE',
// TAIL_BELOW: 'TAIL_BELOW',
// TAIL_LEFT: 'TAIL_LEFT',
// TAIL_RIGHT: 'TAIL_RIGHT',
};
const OUTPUTS = {
U: 'U',
@ -43,10 +43,10 @@ const _BASE_GENOME_SNAKE_BRAIN_NEAT_EDGES = completeBipartiteEdges(
FEATURES.HEAD_Y,
FEATURES.APPLE_REL_X,
FEATURES.APPLE_REL_Y,
FEATURES.TAIL_ABOVE,
FEATURES.TAIL_BELOW,
FEATURES.TAIL_LEFT,
FEATURES.TAIL_RIGHT,
// FEATURES.TAIL_ABOVE,
// FEATURES.TAIL_BELOW,
// FEATURES.TAIL_LEFT,
// FEATURES.TAIL_RIGHT,
],
[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.APPLE_REL_X, appleRel.x);
act.set(FEATURES.APPLE_REL_Y, appleRel.y);
act.set(FEATURES.TAIL_ABOVE, Math.max(...above, 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_RIGHT, Math.max(...right, 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_LEFT, Math.max(...left, BOARD_SQUARES));
// act.set(FEATURES.TAIL_RIGHT, Math.max(...right, BOARD_SQUARES));
this.brain.think(act);

View File

@ -228,7 +228,7 @@
* - 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';
export interface GeneData {
@ -258,6 +258,33 @@ let g_node_id = 1;
let g_innovation_number = 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 }) {
g_node_id = config.node_id;
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
// 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
const allNodeIDs = new Set(nodes.keys());
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>>();
for (const [nodeID, nodeParents] of parents.entries()) {
const nodeParentIDs = setMap(nodeParents, n => n.id);
const nodeIDsAcyclic = setDifference(allNodeIDs, nodeParentIDs);
nodeIDsAcyclic.delete(nodeID);
acyclic.set(nodeID, nodeIDsAcyclic);
const nodeIDsAcyclicNonSource = setDifference(nodeIDsAcyclic, sources);
acyclic.set(nodeID, nodeIDsAcyclicNonSource);
}
// flatten
@ -552,6 +590,9 @@ export interface MutateConfig {
export function mutate(genome: Genome, config: MutateConfig): Genome {
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 => {
if (Math.random() >= mutate_rate) return gene; // this connection should not be mutated
if (Math.random() < assign_rate) {
@ -574,8 +615,10 @@ export function mutate(genome: Genome, config: MutateConfig): Genome {
}
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
const options = findAcyclicNewConns(newGenome);
const options = findAcyclicNonSourceNewConns(newGenome);
if (options.length === 0) {
// TODO: remove this warn once this starts working
// this is mostly a sanity check / useful for metrics
@ -583,10 +626,17 @@ export function mutate(genome: Genome, config: MutateConfig): Genome {
} else {
// choose a random connection
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());
newGenome.push(newGene);
}
findAcyclicNonSourceNewConns(newGenome);
}
console.log('created: ', hashGenome(newGenome), newGenome);
return newGenome;
}
@ -619,6 +669,7 @@ export function computeNextGeneration(
const mom = randchoice(winners);
const dad = chooseMate(mom, winnersPopulation, mcc);
const crossed = crossGenomes(mom, dad, fitness, cc);
// TODO: crossed has cycles!
const baby = mutate(crossed, mc);
// 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>> {
console.log('tracing: ', node.id, depth);
// TODO: there's a particularly nasty issue where maybe nodes are not just from their own genome?
if (depth > 1000) {
if (depth > 20) {
debugger;
}
if (parents.has(node.id)) return parents.get(node.id)!;

View File

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