UNPKG

@alenaksu/neatjs

Version:

NEAT (Neuroevolution of Augmenting Topologies) implementation in JavaScript

268 lines 8.6 kB
import { Organism } from './neat/Organism'; import { Species } from './neat/Species'; import { NodeType } from './types'; /** * Picks n random items from the given list * @param list * @param n */ export function getRandomItems(list, n) { let result = []; let source = [...list]; n = Math.min(n, list.length); while (result.length < n) { let i = Math.floor(rnd(source.length)); result.push(...source.splice(i, 1)); } return result; } /** * Returns a random boolean */ export function randomBool() { return Math.random() > 0.5; } /** * Compute the mean of the given values * @param values */ export function mean(...values) { return values.length ? values.reduce((sum, n) => sum + n, 0) / values.length : 0; } /** * Returns a random number between from/to arguments * @param to * @param from */ export function rnd(to = 1, from = 0) { return Math.random() * (to - from) + from; } /** * Returns a normally distribuited random number (Box-Muller transform) */ export function* gaussian(mean = 0, standardDeviation = 1) { let u, v, s; while (true) { do { v = rnd(1, -1); u = rnd(1, -1); s = u ** 2 + v ** 2; } while (s === 0 || s >= 1); s = Math.sqrt((-2 * Math.log(s)) / s); yield s * u * standardDeviation + mean; yield s * v * standardDeviation + mean; } } /** * Generate a random UUIDv4 (rfc4122 compliant) */ export function uuid() { const uuid = [8, 4, 4, 4, 12].map((segmentLength) => { let segment = Array(segmentLength); for (let i = 0; i < segmentLength; i++) // ToUint32 http://www.ecma-international.org/ecma-262/5.1/#sec-11.7.3 segment[i] = (Math.random() * 0xf) >>> 0; return segment; }); uuid[2][0] &= 0x3; uuid[2][1] |= 0x8; uuid[3][0] = 0x4; return uuid .map((segment) => segment.map(n => n.toString(16)).join('')) .join('-'); } /** * Compute the compatibility distance between two genomes * @param genome1 * @param genome2 * @param config */ export function compatibility(genome1, genome2, config) { // TODO memoizing? consider add an id and use it for that purpose let innovationNumbers = new Set([ ...genome1.connections.keys(), ...genome2.connections.keys() ]); let excess = Math.abs(genome1.connections.size - genome2.connections.size), disjoint = -excess, matching = [], N = Math.max(genome1.connections.size, genome2.connections.size, 1); innovationNumbers.forEach(innovation => { const gene1 = genome1.connections.get(innovation), gene2 = genome2.connections.get(innovation); if (gene1 && gene2) { matching.push(Math.abs(gene1.weight - gene2.weight)); } else if (!gene1 || !gene2) { disjoint++; } }); return ((excess * config.excessCoefficient + disjoint * config.disjointCoefficient) / N + mean(...matching) * config.weightDifferenceCoefficient); } export function sigmoid(x, slope = 4.924273) { return 1 / (1 + Math.exp(-slope * x)); } /** * Check whether the connection creates a loop inside the network * @param connection * @param connections */ export function isRecurrent(connection, connections) { const startNode = connection.from; const stack = [connection]; while (stack.length) { connection = stack.shift(); if (connection.to.id === startNode.id) return true; stack.push(...connections.filter(gene => gene.from.id === connection.to.id)); } return false; } /** * Mate 2 organisms * @param genome1 * @param genome2 */ export function crossover(genome1, genome2, config) { const child = new Organism(); // [moreFit, lessFit] const [hFitParent, lFitParent] = [genome1, genome2].sort(descending((i) => i.fitness)); let innovationNumbers = new Set([ ...hFitParent.connections.keys(), ...lFitParent.connections.keys() ]); // Ensure that all sensors and ouputs are added to the organism hFitParent.nodes.forEach(node => { if (isSensor(node) || isOutput(node)) child.addNode(node.copy()); }); // lFitParent.nodes.forEach(node => { // switch (node.type) { // case NodeType.Input: // case NodeType.Output: // case NodeType.Hidden: // child.addNode(node.copy()); // } // }); Array.from(innovationNumbers.values()) .sort(ascending()) .forEach(innovationNumber => { const hConnection = hFitParent.connections.get(innovationNumber), lConnection = lFitParent.connections.get(innovationNumber); const connection = hConnection && lConnection ? // Matching gene randomBool() && (config.feedForwardOnly && !isRecurrent(hConnection, child.getConnections())) ? hConnection.copy() : lConnection.copy() : // excess/disjoint (hConnection || lConnection).copy(); // Prevent the creation of RNNs if feed-forward only if (config.feedForwardOnly && isRecurrent(connection, child.getConnections())) return; child.connections.set(innovationNumber, connection); connection.from = connection.from.copy(); connection.to = connection.to.copy(); child.addNode(connection.from); child.addNode(connection.to); }); return child; } /** * Perform genome's mutations * @param config * @param organism */ export function mutateGenome(config, organism) { if (rnd() < config.mutateAddNodeProbability) { organism.mutateAddNode(config); } else if (rnd() < config.mutateAddConnectionProbability) { organism.mutateAddConnection(config); } else { if (rnd() < config.mutateConnectionWeightsProbability) organism.mutateConnectionsWeights(config); if (rnd() < config.mutateToggleEnableProbability) organism.mutateToggleEnable(); if (rnd() < config.reEnableGeneProbability) organism.reEnableGene(); } return organism; } /** * Returns a random species form the given set, tending towards better species * @param sortedSpecies A sorted set of species */ export function getRandomSpecies(sortedSpecies) { const random = Math.min(Math.round(gaussian().next().value), 1); const index = wrapNumber(0, sortedSpecies.length - 1, random); // const multiplier = Math.min(gaussian().next().value / 4, 1); // const index = Math.floor(multiplier * (species.length - 1) + 0.5); return sortedSpecies[index]; } /** * Puts the organism inside a compatibile species * @param config * @param organism * @param species */ export function speciateOrganism(config, organism, allSpecies) { const { compatibilityThreshold } = config; const found = allSpecies.length > 0 && allSpecies.some((species) => { if (!species.organisms.length) return false; const isCompatible = compatibility(organism, species.getSpecimen(), config) < compatibilityThreshold; if (isCompatible) species.addOrganism(organism); return isCompatible; }); if (!found) { const species = new Species(); species.addOrganism(organism); allSpecies.push(species); } } /** * Sorts an array from largest to smallest * @param keyFn */ export function descending(keyFn = (i) => i) { return (a, b) => keyFn(b) - keyFn(a); } /** * Sorts an array from smallest to largest * @param keyFn */ export function ascending(keyFn = (i) => i) { return (a, b) => keyFn(a) - keyFn(b); } // TODO export const byFitness = (i) => i.fitness; export const byMaxFitness = (i) => i.maxFitness; export const byInnovation = (i) => i.innovation; export const byType = (i) => i.type; /** * Keeps the given value between the specified range * @param min * @param max * @param value */ export function wrapNumber(min, max, value) { const l = max - min + 1; return ((((value - min) % l) + l) % l) + min; } export const isSensor = (gene) => gene.type === NodeType.Input || gene.type === NodeType.Bias; export const isOutput = (gene) => gene.type === NodeType.Output; /** * Create a new innovation number generator */ export function* Innovation(i = 0) { while (true) yield i++; } //# sourceMappingURL=utils.js.map