generated from michael/webpack-base
fix crossGenomes to prevent cycles
This commit is contained in:
parent
a5884a68db
commit
b33ffc3baa
@ -58,7 +58,7 @@ export interface TrainerSnapshot {
|
||||
|
||||
// labs and mutation -----------------------------------------------------------
|
||||
|
||||
// TODO: random colors for each species
|
||||
// TODO: colors based on species number
|
||||
|
||||
function makeLabColors({ hue, sat, lig }: { hue: number; sat: number; lig: number }): LabColors {
|
||||
const head = `hsl(${hue},${sat}%,${lig}%)`;
|
||||
@ -343,5 +343,5 @@ export default function runCanvas(canvas: HTMLCanvasElement, pipeRef: MutableRef
|
||||
}
|
||||
|
||||
keys.bindKeys();
|
||||
// engine.run(update, render);
|
||||
engine.run(update, render);
|
||||
}
|
||||
|
@ -141,7 +141,7 @@
|
||||
* - Mating:
|
||||
* - for each survivor, repeat fertility[i] times:
|
||||
* - select a mate (dad):
|
||||
* - r_asex chance it is the same organism (TODO: at least one mutation is required?)
|
||||
* - r_asex chance it is the same organism
|
||||
* - else r_int_sp chance it is from a random other organism from any other species
|
||||
* - else it is from a random other organism from the same species
|
||||
* - compute new genome (baby) for mom x dad
|
||||
@ -214,18 +214,6 @@
|
||||
* | 20% | Survival Threshold (your adjusted fitness must be in the top x% to survive to the next generation)
|
||||
* +-------+-------
|
||||
*
|
||||
* --- TODO --------------------------------------------------------------------
|
||||
*
|
||||
* - Determine reproduction algo
|
||||
* - Better understand Explicit Fitness Sharing
|
||||
* - Better understand weights mutation parameter uses
|
||||
*
|
||||
* - Implement Paper!
|
||||
*
|
||||
* --- Implementation Complexities ---------------------------------------------
|
||||
*
|
||||
* - Convert from genome -> neural network topology
|
||||
* - Effectively pass data from inputs, through hidden nodes, to outputs
|
||||
*/
|
||||
|
||||
import { edgesToNodes, NodeID, traceParents, RawEdge, traceChildren } from './network';
|
||||
@ -453,7 +441,7 @@ export function crossGenomes(mom: Genome, dad: Genome, fitness: Map<Genome, numb
|
||||
|
||||
const crossed: Genome = [];
|
||||
|
||||
// matching
|
||||
// matching - randomly selected weight & random chance to re-enable
|
||||
for (const { mom: momGene, dad: dadGene } of alignment.matching) {
|
||||
const newWeight = Math.random() < 0.5 ? momGene.data.weight : dadGene.data.weight;
|
||||
const wasDisabled = !momGene.data.enabled || !dadGene.data.enabled;
|
||||
@ -462,25 +450,15 @@ export function crossGenomes(mom: Genome, dad: Genome, fitness: Map<Genome, numb
|
||||
crossed.push(newGene);
|
||||
}
|
||||
|
||||
// disjoint
|
||||
// disjoint & excess - take from most fit parent
|
||||
const momFitness = fitness.get(mom)!;
|
||||
const dadFitness = fitness.get(dad)!;
|
||||
const mostFit = momFitness === dadFitness ? 'equal' : momFitness > dadFitness ? 'mom' : 'dad';
|
||||
for (const { mom: momGene, dad: dadGene } of alignment.disjoint) {
|
||||
if (momGene === null) crossed.push(dadGene as Gene);
|
||||
else if (dadGene === null) crossed.push(momGene);
|
||||
else if (mostFit === 'mom') crossed.push(momGene);
|
||||
else if (mostFit === 'dad') crossed.push(dadGene);
|
||||
// both are equally fit - select at random
|
||||
else if (Math.random() < 0.5) crossed.push(momGene);
|
||||
else crossed.push(dadGene);
|
||||
}
|
||||
|
||||
// excess
|
||||
for (const { mom: momGene, dad: dadGene } of alignment.excess) {
|
||||
if (momGene === null) crossed.push(dadGene as Gene);
|
||||
else if (dadGene === null) crossed.push(momGene as Gene);
|
||||
else throw Error(`invalid excess alignment: alignment=${JSON.stringify(alignment)}`);
|
||||
if (momFitness > dadFitness) {
|
||||
crossed.push(...alignment.disjoint.flatMap(({ mom: momGene }) => (momGene === null ? [] : momGene)));
|
||||
crossed.push(...alignment.excess.flatMap(({ mom: momGene }) => (momGene === null ? [] : momGene)));
|
||||
} else {
|
||||
crossed.push(...alignment.disjoint.flatMap(({ dad: dadGene }) => (dadGene === null ? [] : dadGene)));
|
||||
crossed.push(...alignment.excess.flatMap(({ dad: dadGene }) => (dadGene === null ? [] : dadGene)));
|
||||
}
|
||||
|
||||
return crossed;
|
||||
@ -535,7 +513,6 @@ export function mutateNewConn(newConn: { src_id: NodeID; dst_id: NodeID }, newWe
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: improve name since also excluding source nodes
|
||||
export function findAcyclicInternalNewConns<DataT>(rawEdges: RawEdge<DataT>[]): { src_id: NodeID; dst_id: NodeID }[] {
|
||||
// finds potential new connections that are acyclic and are not already connected
|
||||
|
||||
@ -548,16 +525,19 @@ export function findAcyclicInternalNewConns<DataT>(rawEdges: RawEdge<DataT>[]):
|
||||
const parents = traceParents(nodes);
|
||||
const children = traceChildren(nodes);
|
||||
|
||||
// exclude *both* sources and sinks
|
||||
// - sources are defined during think, connections will be ignored
|
||||
// - sinks should not be connected? <-- they probably could be, but it would be unclean imo
|
||||
// exclude sources from being dst nodes *and* exclude sinks from being src nodes
|
||||
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] : [])));
|
||||
|
||||
const acyclic = new Map<NodeID, Set<NodeID>>();
|
||||
for (const [nodeID, nodeParents] of parents.entries()) {
|
||||
if (sinks.has(nodeID)) {
|
||||
// exclude sinks from being src nodes
|
||||
acyclic.set(nodeID, new Set());
|
||||
continue;
|
||||
}
|
||||
const nodeParentIDs = setMap(nodeParents, n => n.id);
|
||||
const exclude = setUnion(nodeParentIDs, sources, sinks, new Set([nodeID]));
|
||||
const exclude = setUnion(nodeParentIDs, sources, new Set([nodeID]));
|
||||
const nodeIDsAcyclic = setDifference(allNodeIDs, exclude);
|
||||
acyclic.set(nodeID, nodeIDsAcyclic);
|
||||
}
|
||||
@ -588,7 +568,6 @@ 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);
|
||||
findAcyclicInternalNewConns(genome);
|
||||
|
||||
const newGenome = genome.map(gene => {
|
||||
@ -613,8 +592,6 @@ 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 = findAcyclicInternalNewConns(newGenome);
|
||||
if (options.length === 0) {
|
||||
@ -624,17 +601,12 @@ 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);
|
||||
}
|
||||
|
||||
findAcyclicInternalNewConns(newGenome);
|
||||
}
|
||||
console.log('created: ', hashGenome(newGenome), newGenome);
|
||||
|
||||
return newGenome;
|
||||
}
|
||||
@ -667,7 +639,6 @@ 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
|
||||
|
@ -78,9 +78,8 @@ export function traceParents<DataT>(nodes: Network<Node<DataT>>) {
|
||||
const parents = new Map<NodeID, Set<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 > 20) {
|
||||
// TODO: remove this check, it mostly just finds cycles
|
||||
debugger;
|
||||
}
|
||||
if (parents.has(node.id)) return parents.get(node.id)!;
|
||||
|
@ -267,14 +267,14 @@ function testCrossGenomes() {
|
||||
];
|
||||
|
||||
const fitness = new Map<Genome, number>();
|
||||
fitness.set(genomeA, 2);
|
||||
fitness.set(genomeB, 1);
|
||||
fitness.set(genomeA, 1);
|
||||
fitness.set(genomeB, 2);
|
||||
|
||||
const cc = { reenable_rate: 1 };
|
||||
|
||||
const crossed = crossGenomes(genomeA, genomeB, fitness, cc);
|
||||
|
||||
assert(crossed.length === 10);
|
||||
assert(crossed.length === 9);
|
||||
|
||||
// matching
|
||||
assert(crossed[0]!.src_id === 'A');
|
||||
@ -297,22 +297,13 @@ function testCrossGenomes() {
|
||||
assert(crossed[4]!.data.weight === 5 || crossed[4]!.data.weight === 3);
|
||||
assert(crossed[4]!.data.enabled === true); // always re-enabled since reenable_rate = 1
|
||||
|
||||
// disjoint
|
||||
// disjoint (all from genomeB)
|
||||
assert(crossed[5]!.data.innovation === 6);
|
||||
assert(crossed[5]!.data.weight === 5);
|
||||
|
||||
assert(crossed[6]!.data.innovation === 7);
|
||||
assert(crossed[6]!.data.weight === 5);
|
||||
|
||||
assert(crossed[7]!.data.innovation === 8);
|
||||
assert(crossed[7]!.data.weight === 7);
|
||||
|
||||
// excess
|
||||
assert(crossed[8]!.data.innovation === 9);
|
||||
assert(crossed[8]!.data.weight === 5);
|
||||
|
||||
assert(crossed[9]!.data.innovation === 10);
|
||||
assert(crossed[9]!.data.weight === 5);
|
||||
// excess (all from genomeB)
|
||||
assert(crossed[7]!.data.innovation === 9);
|
||||
assert(crossed[8]!.data.innovation === 10);
|
||||
}
|
||||
addTest(testCrossGenomes);
|
||||
|
||||
@ -380,21 +371,21 @@ function testFindAcyclicInternalNewConns() {
|
||||
const options = findAcyclicInternalNewConns(edges);
|
||||
const expected = [
|
||||
// not A -> B - B is a source
|
||||
// not A -> F - F is a sink
|
||||
{ src_id: 'A', dst_id: 'F' },
|
||||
// not B -> A - A is a source
|
||||
{ src_id: 'B', dst_id: 'C' },
|
||||
// not B -> E - E is a sink
|
||||
// not B -> F - F is a sink
|
||||
{ src_id: 'B', dst_id: 'E' },
|
||||
{ src_id: 'B', dst_id: 'F' },
|
||||
// not C -> B - B is a source
|
||||
{ src_id: 'C', dst_id: 'D' },
|
||||
// not C -> F - F is a sink
|
||||
{ src_id: 'C', dst_id: 'F' },
|
||||
{ src_id: 'D', dst_id: 'C' },
|
||||
// not E -> B - B is a parent of E
|
||||
// not E -> F - F is a sink
|
||||
// not F -> A - A is a parent of F
|
||||
// not F -> B - B is a parent of F
|
||||
{ src_id: 'F', dst_id: 'C' },
|
||||
// not F -> E - E is a sink
|
||||
// not F -> A - F is a sink
|
||||
// not F -> B - F is a sink
|
||||
// not F -> C - F is a sink
|
||||
// not F -> E - F is a sink
|
||||
];
|
||||
|
||||
function strcmp(a: string, b: string) {
|
||||
|
Loading…
Reference in New Issue
Block a user