diff --git a/README.md b/README.md index 279d82ee..da94b9a9 100644 --- a/README.md +++ b/README.md @@ -247,3 +247,13 @@ Whisker is supported by the project FR 2955/3-1 funded by the doi = {10.48550/arXiv.2304.06413} } ``` + +``` +@inproceedings{nuzzlebug24, + author = {Adina Deiner and Gordon Fraser}, + title = {NuzzleBug: Debugging Block-Based Programs in Scratch }, + booktitle = {ACM/IEEE International Conference on Software Engineering (ICSE)}, + publisher = {{IEEE}}, + year = {2024} +} +``` diff --git a/config/Neuroevolution/neatest.json b/config/Neuroevolution/neatest.json index 678bbf2f..ad0f57e4 100644 --- a/config/Neuroevolution/neatest.json +++ b/config/Neuroevolution/neatest.json @@ -2,12 +2,12 @@ "testGenerator": "neuroevolution", "algorithm": "neatest", "eventSelection": "activation", - "populationSize": 300, - "numberOfSpecies": 10, + "populationSize": 150, + "numberOfSpecies": 5, "parentsPerSpecies": 0.20, "penalizingAge": 15, "ageSignificance": 1.0, - "switchTargetCount": 5, + "switchTargetCount": 10, "extractor": "neuroevolution", "aTRepetitions": 0, "chromosome": { @@ -26,12 +26,12 @@ "mutationWithoutCrossover": 0.25, "mutationAddConnection": 0.05, "recurrentConnection": 0.1, - "addConnectionTries": 50, + "addConnectionTries": 10, "populationChampionNumberOffspring": 3, "populationChampionNumberClones": 1, "populationChampionConnectionMutation": 0.3, "mutationAddNode": 0.03, - "mutateWeights": 0.5, + "mutateWeights": 0.8, "perturbationPower": 1, "mutateToggleEnableConnection": 0.1, "toggleEnableConnectionTimes": 3, @@ -51,11 +51,11 @@ "type": "reliableStatement", "timeout": 10000, "stableCount": 10, - "earlyStop": true + "earlyStop": false }, "population": { "strategy": "global_solutions", - "randomFraction": 0.1 + "randomFraction": 0.3 }, "stoppingCondition": { "type": "combined", diff --git a/config/Neuroevolution/neatestBackprop.json b/config/Neuroevolution/neatestBackprop.json index 880c0be0..a007dcc1 100644 --- a/config/Neuroevolution/neatestBackprop.json +++ b/config/Neuroevolution/neatestBackprop.json @@ -2,7 +2,7 @@ "testGenerator": "neuroevolution", "algorithm": "neatest", "eventSelection": "activation", - "populationSize": 100, + "populationSize": 150, "numberOfSpecies": 5, "parentsPerSpecies": 0.20, "penalizingAge": 15, @@ -21,7 +21,7 @@ "epochs": 1000, "batchSize": 1, "labelSmoothing": 0, - "probability": 1, + "probability": 0.6, "peerToPeerSharing": false, "dataAugmentation": { @@ -42,7 +42,7 @@ "mutationWithoutCrossover": 0.25, "mutationAddConnection": 0.05, "recurrentConnection": 0.1, - "addConnectionTries": 50, + "addConnectionTries": 10, "populationChampionNumberOffspring": 3, "populationChampionNumberClones": 1, "populationChampionConnectionMutation": 0.3, diff --git a/whisker-main/src/whisker/testcase/NeuroevolutionScratchEventExtractor.ts b/whisker-main/src/whisker/testcase/NeuroevolutionScratchEventExtractor.ts index 9abc1c68..65231a35 100644 --- a/whisker-main/src/whisker/testcase/NeuroevolutionScratchEventExtractor.ts +++ b/whisker-main/src/whisker/testcase/NeuroevolutionScratchEventExtractor.ts @@ -1,7 +1,6 @@ import {ScratchBlocks} from "./ScratchEventExtractor"; import VirtualMachine from "scratch-vm/src/virtual-machine"; import {ScratchEvent} from "./events/ScratchEvent"; -import {DragSpriteEvent} from "./events/DragSpriteEvent"; import {KeyPressEvent} from "./events/KeyPressEvent"; import {MouseMoveEvent} from "./events/MouseMoveEvent"; import {Container} from "../utils/Container"; @@ -112,7 +111,7 @@ export class NeuroevolutionScratchEventExtractor extends DynamicScratchEventExtr const currentMousePosition = Container.vmWrapper.inputs.getMousePos(); // Only add a MouseMoveTo event if the mouse is currently not located at the targeted position. if (currentMousePosition.x !== target.x || currentMousePosition.y !== target.y) { - eventList.push(new MouseMoveToEvent(target.x, target.y)); + eventList.push(new MouseMoveToEvent(target.x, target.y, target.sprite.name)); } eventList.push(new MouseMoveEvent()); } diff --git a/whisker-main/src/whisker/testcase/ScratchEventExtractor.ts b/whisker-main/src/whisker/testcase/ScratchEventExtractor.ts index 6886df09..362c5786 100644 --- a/whisker-main/src/whisker/testcase/ScratchEventExtractor.ts +++ b/whisker-main/src/whisker/testcase/ScratchEventExtractor.ts @@ -249,7 +249,7 @@ export abstract class ScratchEventExtractor { const currentMousePosition = Container.vmWrapper.inputs.getMousePos(); // Only add a MouseMoveTo event if the mouse is currently not located at the targeted position. if (currentMousePosition.x !== target.x || currentMousePosition.y !== target.y) { - eventList.push(new MouseMoveToEvent(target.x, target.y)); + eventList.push(new MouseMoveToEvent(target.x, target.y, target.sprite.name)); } eventList.push(new MouseMoveEvent()); } diff --git a/whisker-main/src/whisker/testcase/events/MouseMoveToEvent.ts b/whisker-main/src/whisker/testcase/events/MouseMoveToEvent.ts index 7f75e4b2..47e59707 100644 --- a/whisker-main/src/whisker/testcase/events/MouseMoveToEvent.ts +++ b/whisker-main/src/whisker/testcase/events/MouseMoveToEvent.ts @@ -24,32 +24,34 @@ import {Container} from "../../utils/Container"; export class MouseMoveToEvent extends ScratchEvent { - private readonly x: number; - private readonly y: number; + private _x: number; + private _y: number; + private readonly _sprite: string; - constructor(x: number, y: number) { + constructor(x: number, y: number, sprite: string) { super(); - this.x = x; - this.y = y; + this._x = x; + this._y = y; + this._sprite = sprite; } async apply(): Promise { - Container.testDriver.mouseMove(this.x, this.y); + Container.testDriver.mouseMove(this._x, this._y); } public toJavaScript(): string { - return `t.mouseMove(${Math.trunc(this.x)}, ${Math.trunc(this.y)});`; + return `t.mouseMove(${Math.trunc(this._x)}, ${Math.trunc(this._y)});`; } public toJSON(): Record { const event = {}; event[`type`] = `MouseMoveToEvent`; - event[`args`] = {"x": this.x, "y": this.y}; + event[`args`] = {"x": this._x, "y": this._y}; return event; } public toString(): string { - return "MouseMoveToEvent " + Math.trunc(this.x) + "/" + Math.trunc(this.y); + return "MouseMoveToEvent " + Math.trunc(this._x) + "/" + Math.trunc(this._y); } numSearchParameter(): number { @@ -57,7 +59,7 @@ export class MouseMoveToEvent extends ScratchEvent { } getParameters(): [number, number] { - return [this.x, this.y]; + return [this._x, this._y]; } getSearchParameterNames(): [] { @@ -69,6 +71,18 @@ export class MouseMoveToEvent extends ScratchEvent { } stringIdentifier(): string { - return `MouseMoveToEvent-${this.x}-${this.y}`; + return `MouseMoveToEvent-${this.sprite}`; + } + + get sprite(): string { + return this._sprite; + } + + set x(value: number) { + this._x = value; + } + + set y(value: number) { + this._y = value; } } diff --git a/whisker-main/src/whisker/whiskerNet/Algorithms/Neatest.ts b/whisker-main/src/whisker/whiskerNet/Algorithms/Neatest.ts index 5f5d1dbd..82aa0ca5 100644 --- a/whisker-main/src/whisker/whiskerNet/Algorithms/Neatest.ts +++ b/whisker-main/src/whisker/whiskerNet/Algorithms/Neatest.ts @@ -50,7 +50,7 @@ export class Neatest extends NEAT { /** * Saves target ids of objectives that have already been switched out in order to prioritise different ones. */ - private _switchedTargets: string[] = []; + private _switchedTargets = new Set(); /** * Holds a record of promising targets, i.e. the maximum amount of how often a target has accidentally already been @@ -60,7 +60,7 @@ export class Neatest extends NEAT { /** * Searches for a suite of networks that are able to cover all statements of a given Scratch program reliably. - * @returns a Map mapping a statement's key to the network capable of reaching the given statement reliably. + * @returns Mapping of a statement's key to the network capable of reaching the given statement reliably. */ override async findSolution(): Promise> { this.initialise(); @@ -94,12 +94,12 @@ export class Neatest extends NEAT { // Switch the target if we stop improving for a set number of times and have statements to which we // can switch to left const uncoveredStatementIds = this.getUncoveredStatements().map(statement => statement.getTargetNode().id); - const uncoveredUntouchedTargets = uncoveredStatementIds.filter(targetId => !this._switchedTargets.includes(targetId)); + const uncoveredUntouchedTargets = uncoveredStatementIds.filter(targetId => !this._switchedTargets.has(targetId)); if (this._population.highestFitnessLastChanged >= this._neuroevolutionProperties.switchTargetCount && uncoveredUntouchedTargets.length > 0) { const currentTargetId = this._fitnessFunctionMap.get(this._targetKey).getTargetNode().id; - this._switchedTargets.push(currentTargetId); - Container.debugLog("Switching Target due to missing improvement."); + this._switchedTargets.add(currentTargetId); + Container.debugLog("Switching Target " + currentTargetId + " due to missing improvement."); break; } @@ -159,7 +159,7 @@ export class Neatest extends NEAT { // were not selected as target yet. if (nextTarget === undefined) { const uncoveredUntouchedTargets = new Set([...potentialTargets].filter(target => - !this._switchedTargets.includes(target.getTargetNode().id))); + !this._switchedTargets.has(target.getTargetNode().id))); potentialTargets = uncoveredUntouchedTargets.size > 0 ? uncoveredUntouchedTargets : potentialTargets; let mostPromisingTargets = []; let mostPromisingValue = 0; @@ -185,7 +185,7 @@ export class Neatest extends NEAT { } } - // If no target looks promising we pick the next target randomly. + // If no target looks promising, we pick the next target randomly. if (nextTarget === undefined) { nextTarget = Randomness.getInstance().pick(Array.from(potentialTargets)); } @@ -249,7 +249,7 @@ export class Neatest extends NEAT { // reached a previously not targeted statement without reaching the actual target statement at least once. if (this._promisingTargets.get(this._targetKey) < 1) { const uncoveredTargetIds = this.getUncoveredStatements().map(target => target.getTargetNode().id); - const untouchedUncovered = uncoveredTargetIds.filter(target => !this._switchedTargets.includes(target)); + const untouchedUncovered = uncoveredTargetIds.filter(target => !this._switchedTargets.has(target)); for (const [key, value] of this._promisingTargets) { if (value > 0 && untouchedUncovered.includes(this._fitnessFunctionMap.get(key).getTargetNode().id)) { this._switchToEasierTarget = true; @@ -315,11 +315,8 @@ export class Neatest extends NEAT { const fitnessFunction = this._fitnessFunctions.get(fitnessFunctionKey); const coverageStableCount = network.openStatementTargets.get(fitnessFunctionKey); const statementFitness = await fitnessFunction.getFitness(network); - return (coverageStableCount >= this._neuroevolutionProperties.coverageStableCount && - !this._archive.has(fitnessFunctionKey)) || - (this._neuroevolutionProperties.coverageStableCount == 0 && - await fitnessFunction.isOptimal(statementFitness) && - !this._archive.has(fitnessFunctionKey)); + return (coverageStableCount >= this._neuroevolutionProperties.coverageStableCount && !this._archive.has(fitnessFunctionKey)) || + (this._neuroevolutionProperties.coverageStableCount == 0 && await fitnessFunction.isOptimal(statementFitness) && !this._archive.has(fitnessFunctionKey)); } /** @@ -333,11 +330,14 @@ export class Neatest extends NEAT { Container.debugLog(`Best Network Fitness: ${this._population.bestFitness}`); Container.debugLog(`Current Iteration Best Network Fitness: ${this._population.populationChampion.fitness}`); Container.debugLog(`Average Network Fitness: ${this._population.averageFitness}`); - Container.debugLog(`Generations passed since last improvement: ${this._population.highestFitnessLastChanged}`); - const sortedSpecies = this._population.species.sort((a, b) => b.averageFitness - a.averageFitness); + + const sortedSpecies = this._population.species.sort((a, b) => b.uID - a.uID); + Container.debugLog(`Population of ${this._population.populationSize} distributed in ${sortedSpecies.length} species`); + Container.debugLog("\tID\tage\tsize\tfitness\tshared fitness"); for (const species of sortedSpecies) { - Container.debugLog(`Species ${species.uID} has ${species.networks.length} members and an average fitness of ${species.averageFitness}`); + Container.debugLog(`\t${species.uID}\t${species.age}\t${species.networks.length}\t${Math.round(species.averageFitness * 100) / 100}\t${Math.round(species.averageSharedFitness * 100) / 100}`); } + Container.debugLog(`Generations passed since last improvement: ${this._population.highestFitnessLastChanged}`); Container.debugLog(`Time passed in seconds: ${(Date.now() - this.getStartTime())}`); Container.debugLog("\n-----------------------------------------------------\n"); } diff --git a/whisker-main/src/whisker/whiskerNet/HyperParameter/NeuroevolutionTestGenerationParameter.ts b/whisker-main/src/whisker/whiskerNet/HyperParameter/NeuroevolutionTestGenerationParameter.ts index fc2e060d..57bc3ff2 100644 --- a/whisker-main/src/whisker/whiskerNet/HyperParameter/NeuroevolutionTestGenerationParameter.ts +++ b/whisker-main/src/whisker/whiskerNet/HyperParameter/NeuroevolutionTestGenerationParameter.ts @@ -19,12 +19,12 @@ export class NeuroevolutionTestGenerationParameter extends BasicNeuroevolutionPa private _numberOfSpecies = 5; /** - * Specifies how many member per species survive in each generation. + * Specifies how many members per species survive in each generation. */ private _parentsPerSpecies = 0.20; /** - * Specifies when Species start go get penalized if no improvement is being observed. + * Specifies when Species start to get penalised if no improvement is being observed. */ private _penalizingAge = 15; diff --git a/whisker-main/src/whisker/whiskerNet/Misc/NetworkExecutor.ts b/whisker-main/src/whisker/whiskerNet/Misc/NetworkExecutor.ts index 58e4fbb7..b9933d7c 100644 --- a/whisker-main/src/whisker/whiskerNet/Misc/NetworkExecutor.ts +++ b/whisker-main/src/whisker/whiskerNet/Misc/NetworkExecutor.ts @@ -103,8 +103,9 @@ export class NetworkExecutor { // Update input nodes and load inputs into the Network. const spriteFeatures = InputExtraction.extractFeatures(this._vm); - // Check if we encountered additional events during the playthrough - // If we did so add corresponding ClassificationNodes and RegressionNodes to the network. + // Check if we encountered additional sprites/events during the playthrough + // If we did so add corresponding input/output nodes to the network. + network.updateInputNodes(spriteFeatures); network.updateOutputNodes(this.availableEvents); const defect = !network.activateNetwork(spriteFeatures); diff --git a/whisker-main/src/whisker/whiskerNet/Misc/StateActionRecorder.ts b/whisker-main/src/whisker/whiskerNet/Misc/StateActionRecorder.ts index 061c05af..71ec0fed 100644 --- a/whisker-main/src/whisker/whiskerNet/Misc/StateActionRecorder.ts +++ b/whisker-main/src/whisker/whiskerNet/Misc/StateActionRecorder.ts @@ -136,7 +136,7 @@ export class StateActionRecorder extends EventEmitter { // Check if event is present at all. Always include typeTextEvents since they can only be emitted if a // question was asked. - if (availableActions.some(actionId => actionId.localeCompare(event.stringIdentifier(), 'en', { sensitivity: 'base' }) === 0) || + if (availableActions.some(actionId => actionId.localeCompare(event.stringIdentifier(), 'en', {sensitivity: 'base'}) === 0) || event instanceof TypeTextEvent || event instanceof TypeNumberEvent) { this._recordAction(event); } @@ -239,7 +239,7 @@ export class StateActionRecorder extends EventEmitter { clearInterval(this._checkForMouseMoveInterval); this._stateAtAction.delete(this.MOUSE_MOVE_ACTION_KEY); event = new ClickSpriteEvent(clickTarget); - } else if (availableActions.includes(new MouseDownForStepsEvent().stringIdentifier())){ + } else if (availableActions.includes(new MouseDownForStepsEvent().stringIdentifier())) { // Check if we had a long period without any actions being executed. this._checkForWait(false); // Register mouse down Event and @@ -269,8 +269,8 @@ export class StateActionRecorder extends EventEmitter { (stepsSinceLastMouseMove > this.MOUSE_MOVE_THRESHOLD || mouseDownNoticed)) { const clickTarget = Util.getTargetSprite(this._vm); let event: ScratchEvent; - if (availableActions.includes(new MouseMoveToEvent(clickTarget.x, clickTarget.y).stringIdentifier())) { - event = new MouseMoveToEvent(clickTarget.x, clickTarget.y); + if (availableActions.includes(new MouseMoveToEvent(clickTarget.x, clickTarget.y, clickTarget.sprite.name).stringIdentifier())) { + event = new MouseMoveToEvent(clickTarget.x, clickTarget.y, clickTarget.sprite.name); } else { event = new MouseMoveEvent(this._mouseCoordinates[0], this._mouseCoordinates[1]); } @@ -317,6 +317,16 @@ export class StateActionRecorder extends EventEmitter { } else { stateFeatures = InputExtraction.extractFeatures(this._vm); } + + // Reduce required storage capacity by rounding state values. + for (const featureGroup of stateFeatures.values()) { + for (const [feature, value] of featureGroup.entries()) { + console.log("Prev Feature " + featureGroup.get(feature)); + featureGroup.set(feature, Math.round(value * 100) / 100); + console.log("After feature " + featureGroup.get(feature)); + } + } + let parameter: Record; switch (event.toJSON()['type']) { case "WaitEvent": @@ -348,6 +358,13 @@ export class StateActionRecorder extends EventEmitter { console.log("Missing event handler: ", event); } + // Reduce required storage capacity by rounding action parameter. + for (const key in parameter) { + console.log("PRev Param: " + parameter[key]); + parameter[key] = Math.round(parameter[key] * 100) / 100; + console.log("After Param: " + parameter[key]); + } + const record: ActionRecord = { state: stateFeatures, action: action, diff --git a/whisker-main/src/whisker/whiskerNet/NetworkFitness/ReliableStatementFitness.ts b/whisker-main/src/whisker/whiskerNet/NetworkFitness/ReliableStatementFitness.ts index f23d3660..63e493a8 100644 --- a/whisker-main/src/whisker/whiskerNet/NetworkFitness/ReliableStatementFitness.ts +++ b/whisker-main/src/whisker/whiskerNet/NetworkFitness/ReliableStatementFitness.ts @@ -81,13 +81,14 @@ export class ReliableStatementFitness implements NetworkFitnessFunction { @@ -26,8 +25,8 @@ export class NeatChromosomeGenerator implements ChromosomeGenerator(); layer.set(0, []); @@ -64,7 +62,7 @@ export class NeatChromosomeGenerator implements ChromosomeGenerator event.numSearchParameter() > 0); if (parameterizedEvents.length !== 0) { this.addRegressionNodes(numNodes, layer, parameterizedEvents); @@ -74,13 +72,8 @@ export class NeatChromosomeGenerator implements ChromosomeGenerator node.depth)); + + for (const [sprite, featureMap] of this.inputNodes.entries()) { + // Add Hidden Node if there is none for the given sprite feature. + if (!this._fullyHiddenPairs.has(sprite)) { + const depth = minDepth / 2; + const hiddenNode = new HiddenNode(++NeatPopulation.highestNodeId, depth, this.activationFunction); + this.addNode(hiddenNode); + this._fullyHiddenPairs.set(sprite, hiddenNode); } - } - for (const featureMap of this.inputNodes.values()) { - const depth = this.getDepthOfNewNode([...featureMap.values()][0], minDepthNode); - const hiddenNode = new HiddenNode(this.getNumNodes(), depth, this.activationFunction); - this.addNode(hiddenNode, [...featureMap.values()][0], minDepthNode); - for (const inputNode of featureMap.values()) { - const inputHiddenConnection = new ConnectionGene(inputNode, hiddenNode, this._random.nextDoubleMinMax(-1, 1), true, 0); - this.addConnection(inputHiddenConnection); + + const hiddenNode = this._fullyHiddenPairs.get(sprite); + const hiddenIncomingNodes = hiddenNode.incomingConnections.map(conn => conn.source); + + // Connect inputNode to hiddenNode if there is no such connection. + for (const iNode of featureMap.values()) { + if (!hiddenIncomingNodes.includes(iNode)) { + const inputHiddenConn = new ConnectionGene(iNode, hiddenNode, this._random.nextDoubleMinMax(-1, 1), true, 0); + newConnections.push(inputHiddenConn); + this.addConnection(inputHiddenConn); + } } + + // Connect nodeToConnect to corresponding hidden node. for (const nodeToConnect of nodesToConnect) { - const hiddenOutputConnection = new ConnectionGene(hiddenNode, nodeToConnect, this._random.nextDoubleMinMax(-1, 1), true, 0); - this.addConnection(hiddenOutputConnection); + const hiddenToNewNode = new ConnectionGene(hiddenNode, nodeToConnect, this._random.nextDoubleMinMax(-1, 1), true, 0); + newConnections.push(hiddenToNewNode); + this.addConnection(hiddenToNewNode); } + } - return connections; + + // Connect new nodes to Bias + const biasNode = this.layers.get(0).find(node => node.type == NodeType.BIAS); + for (const nodeToConnect of nodesToConnect) { + const biasConnection = new ConnectionGene(biasNode, nodeToConnect, this._random.nextDoubleMinMax(-1, 1), true, 0); + newConnections.push(biasConnection); + this.addConnection(biasConnection); + } + + return newConnections; } /** @@ -299,7 +323,7 @@ export class NeatChromosome extends NetworkChromosome { connection1 = new ConnectionGene(sourceNode, newNode, 1.0, true, innovation.firstInnovationNumber); connection2 = new ConnectionGene(newNode, targetNode, oldWeight, true, innovation.secondInnovationNumber); } else { - const nextNodeId = NeatPopulation.highestNodeId + 1; + const nextNodeId = ++NeatPopulation.highestNodeId; newNode = new HiddenNode(nextNodeId, depth, activationFunction); const newInnovation: AddNodeSplitConnectionInnovation = { @@ -318,7 +342,7 @@ export class NeatChromosome extends NetworkChromosome { // We do not use the addConnection method here since we have already assigned innovation numbers to the // created connections. - this.addNode(newNode, sourceNode, targetNode); + this.addNode(newNode); this.connections.push(connection1); this.connections.push(connection2); this.generateNetwork(); diff --git a/whisker-main/src/whisker/whiskerNet/Networks/NetworkChromosome.ts b/whisker-main/src/whisker/whiskerNet/Networks/NetworkChromosome.ts index 597d4e1c..452a33c0 100644 --- a/whisker-main/src/whisker/whiskerNet/Networks/NetworkChromosome.ts +++ b/whisker-main/src/whisker/whiskerNet/Networks/NetworkChromosome.ts @@ -17,6 +17,8 @@ import assert from "assert"; import {FeatureGroup, InputFeatures} from "../Misc/InputExtraction"; import {eventAndParametersObject, ObjectInputFeatures, StateActionRecord} from "../Misc/GradientDescent"; import {BiasNode} from "../NetworkComponents/BiasNode"; +import {HiddenNode} from "../NetworkComponents/HiddenNode"; +import {MouseMoveToEvent} from "../../testcase/events/MouseMoveToEvent"; export abstract class NetworkChromosome extends Chromosome { @@ -45,6 +47,13 @@ export abstract class NetworkChromosome extends Chromosome { */ protected readonly _regressionNodes = new Map(); + /** + * When using the fullyHidden input connection method, this map keeps track of which input nodes + * are connected to which HiddenNodes during the generation of the chromosome. This mapping facilitates connecting + * new output nodes to the networks during the execution. + */ + protected readonly _fullyHiddenPairs: Map = new Map(); + /** * Reference activation trace serving as the ground truth. */ @@ -222,15 +231,31 @@ export abstract class NetworkChromosome extends Chromosome { public updateOutputNodes(events: ScratchEvent[]): void { let updated = false; for (const event of events) { + + // Update MouseMoveEvents by changing the Event itself in order to prevent an explosion of such events. + if (event instanceof MouseMoveToEvent) { + const targetSprite = event.sprite; + for (const classNode of this.classificationNodes.values()) { + const nodeEvent = classNode.event; + if (nodeEvent instanceof MouseMoveToEvent && nodeEvent.sprite === targetSprite + && (nodeEvent.x !== event.x || nodeEvent.y !== event.y)) { + nodeEvent.x = event.x; + nodeEvent.y = event.y; + break; + } + } + } + + // Check if we have to add new event nodes. if (!this.classificationNodes.has(event.stringIdentifier())) { updated = true; const featureID = `C:${event.stringIdentifier()}`; const id = NetworkChromosome.getNonHiddenNodeId(featureID); const classificationNode = new ClassificationNode(id, event, ActivationFunction.NONE); this._layers.get(1).push(classificationNode); - this.connectNodeToInputLayer([classificationNode], this._inputConnectionMethod); + this.connectNodesToInputLayer([classificationNode], this._inputConnectionMethod); } - // Check if we also have to add regression nodes. + // Check if we also have to add new regression nodes. if (!this.regressionNodes.has(event.stringIdentifier()) && event.numSearchParameter() > 0) { updated = true; for (const parameter of event.getSearchParameterNames()) { @@ -238,7 +263,7 @@ export abstract class NetworkChromosome extends Chromosome { const id = NetworkChromosome.getNonHiddenNodeId(featureID); const regressionNode = new RegressionNode(id, event, parameter); this._layers.get(1).push(regressionNode); - this.connectNodeToInputLayer([regressionNode], this._inputConnectionMethod); + this.connectNodesToInputLayer([regressionNode], this._inputConnectionMethod); } } } @@ -253,7 +278,7 @@ export abstract class NetworkChromosome extends Chromosome { * @param nodesToConnect the nodes that should be connected to the input layer. * @param mode determines how the input layer should be connected to the given nodes. */ - public abstract connectNodeToInputLayer(nodesToConnect: NodeGene[], mode: InputConnectionMethod): void; + public abstract connectNodesToInputLayer(nodesToConnect: NodeGene[], mode: InputConnectionMethod): void; /** * Fetches the ID of a functional Node, i.e. a non-Hidden node. @@ -357,11 +382,9 @@ export abstract class NetworkChromosome extends Chromosome { this._calculateNodeValue(node); node.activationValue = node.activate(); } - } - - // For output nodes calculate the node values first since we require them for the softmax function within - // the classification nodes. - else { + } else { + // For output nodes, calculate the node values first since we require them for the softmax function within + // the classification nodes. for (const node of nodes) { this._calculateNodeValue(node); } @@ -593,11 +616,9 @@ export abstract class NetworkChromosome extends Chromosome { /** * Adds a new node to the network. * @param newNode the node to be added. - * @param sourceNode the source node with a connection into the new node. - * @param targetNode the target node to which the new node has an outgoing connection. */ - public addNode(newNode: NodeGene, sourceNode: NodeGene, targetNode: NodeGene): void { - const depth = this.getDepthOfNewNode(sourceNode, targetNode); + public addNode(newNode: NodeGene): void { + const depth = newNode.depth; if (!this._layers.get(depth)) { this._layers.set(depth, []); } diff --git a/whisker-main/src/whisker/whiskerNet/NeuroevolutionPopulations/NeatPopulation.ts b/whisker-main/src/whisker/whiskerNet/NeuroevolutionPopulations/NeatPopulation.ts index 8f41f436..67830de0 100644 --- a/whisker-main/src/whisker/whiskerNet/NeuroevolutionPopulations/NeatPopulation.ts +++ b/whisker-main/src/whisker/whiskerNet/NeuroevolutionPopulations/NeatPopulation.ts @@ -87,21 +87,26 @@ export class NeatPopulation extends NeuroevolutionPopulation { * Generates a new generation of networks by evolving the current population. */ public evolve(): void { + // Remove chromosomes that are not allowed to reproduce. + const doomedChromosomes = []; for (const chromosome of this.networks) { if (!chromosome.isParent) { const specie = chromosome.species; specie.removeNetwork(chromosome); - this.removeNetwork(chromosome); + doomedChromosomes.push(chromosome); } } + this._networks = this.networks.filter(network => !doomedChromosomes.includes(network)); + // Now, let the reproduction start. const offspring: NeatChromosome[] = []; for (const specie of this.species) { offspring.push(...specie.evolve(this, this.species)); } - // Speciate the produced offspring + // Assign representatives to each species and assign each offspring to its closest matching species. + this.assignRepresentatives(offspring); for (const child of offspring) { this.speciate(child); } @@ -134,16 +139,6 @@ export class NeatPopulation extends NeuroevolutionPopulation { Arrays.remove(this.species, doomedSpecie); } this.generation++; - - // If we have big differences in fitness values across species, we might get small over-populations that expand. - if (this.networks.length > this.hyperParameter.populationSize) { - Container.debugLog(`The population size has changed from ${this.hyperParameter.populationSize} to - ${this.networks.length} members.`); - while (this.networks.length > this.hyperParameter.populationSize) { - this.networks.pop(); - } - Container.debugLog(`Reduced the population size down to ${this.networks.length}`); - } } /** @@ -238,7 +233,7 @@ export class NeatPopulation extends NeuroevolutionPopulation { this.highestFitnessLastChanged++; } - // If there is a stagnation in fitness refocus the search + // If there is a stagnation in fitness, refocus the search if (this.highestFitnessLastChanged > this.hyperParameter.penalizingAge + 5) { Container.debugLog("Refocusing the search on the two most promising species"); this.highestFitnessLastChanged = 0; @@ -284,35 +279,55 @@ export class NeatPopulation extends NeuroevolutionPopulation { return this.averageFitness; } + /** + * Assigns a new representative from the offspring for each species based on the compatibility distance + * to the previous representative. + * @param offspring the offspring from which representatives will be selected. + */ + public assignRepresentatives(offspring: NeatChromosome[]): void { + for (const specie of this.species) { + let minDistance = Number.MAX_VALUE; + let closestNetwork = offspring[0]; + for (const network of offspring) { + const distance = this.compatibilityDistance(specie.representative, network); + if (distance < minDistance) { + minDistance = distance; + closestNetwork = network; + } + } + specie.representative = closestNetwork; + } + } + /** * Assigns a network to the first compatible species. * @param network the network that should be assigned to a species. */ public speciate(network: NeatChromosome): void { - // If we have no existent species in our population create the first one. + // If we have no existent species in our population, create the first one. if (this.species.length === 0) { const newSpecies = new Species(this.speciesCount, true, this.hyperParameter); + newSpecies.representative = network; this.speciesCount++; this.species.push(newSpecies); newSpecies.networks.push(network); network.species = newSpecies; - } - - // If we already have some species find a compatible one or create a new species for the network if the network - // is not compatible enough with any existent species. - else { + } else { + // If we already have some species, + // find a compatible one or create a new species for the network if the network + // is not compatible enough with any existent species. let foundSpecies = false; for (const specie of this.species) { // Skip empty species if (specie.networks.length == 0) { continue; } + // Get a representative of the specie and calculate the compatibility distance. - const representative = specie.networks[0]; - const compatDistance = this.compatibilityDistance(network, representative); + const compatDistance = this.compatibilityDistance(network, specie.representative); - // If the representative and the given network are compatible enough add the network to the + // If the representative and the given network are compatible enough, add the network to the // representative's species. if (compatDistance < this.compatibilityThreshold) { specie.networks.push(network); @@ -322,9 +337,10 @@ export class NeatPopulation extends NeuroevolutionPopulation { } } - // If the network fits into no species create a new one. + // If the network fits into no species, create a new one. if (!foundSpecies) { const newSpecies = new Species(this.speciesCount, true, this.hyperParameter); + newSpecies.representative = network; this.speciesCount++; this.species.push(newSpecies); newSpecies.networks.push(network); @@ -370,7 +386,7 @@ export class NeatPopulation extends NeuroevolutionPopulation { // and their weight differences. for (let i = 0; i < maxSize; i++) { - // If we exceeded the size of one of the two network, we have an excess gene. + // If we exceeded the size of one of the two networks, we have an excess gene. if (i1 >= size1) { excess++; i2++; diff --git a/whisker-main/src/whisker/whiskerNet/NeuroevolutionPopulations/NeuroevolutionPopulation.ts b/whisker-main/src/whisker/whiskerNet/NeuroevolutionPopulations/NeuroevolutionPopulation.ts index 6a00794a..be35a19b 100644 --- a/whisker-main/src/whisker/whiskerNet/NeuroevolutionPopulations/NeuroevolutionPopulation.ts +++ b/whisker-main/src/whisker/whiskerNet/NeuroevolutionPopulations/NeuroevolutionPopulation.ts @@ -22,7 +22,7 @@ export abstract class NeuroevolutionPopulation { /** * Saves all networks of the current population. */ - private readonly _networks: C[] = []; + protected _networks: C[] = []; /** * The average fitness of the current generation. Used for reporting purposes. diff --git a/whisker-main/src/whisker/whiskerNet/NeuroevolutionPopulations/Species.ts b/whisker-main/src/whisker/whiskerNet/NeuroevolutionPopulations/Species.ts index bca7da2b..3e08a680 100644 --- a/whisker-main/src/whisker/whiskerNet/NeuroevolutionPopulations/Species.ts +++ b/whisker-main/src/whisker/whiskerNet/NeuroevolutionPopulations/Species.ts @@ -22,18 +22,23 @@ export class Species { */ private readonly _networks: C[] = [] + /** + * The representative of this species used for speciation calculations. + */ + private _representative: C; + /** * The age of the species. */ private _age = 1; /** - * Average fitness across all member of the species. + * Average fitness across all members of the species. */ private _averageFitness = 0; /** - * Average shared fitness across all member of the species. + * Average shared fitness across all members of the species. */ private _averageSharedFitness = 0; @@ -97,8 +102,8 @@ export class Species { * Assigns the shared fitness value to each member of the species. */ public assignSharedFitness(): void { - // Calculate the age debt based on the penalizing factor -> Determines after how many generations of no - // improvement the species gets penalized + // Calculate the age debt based on the penalising factor -> Determines after how many generations of no + // improvement the species gets penalised let ageDept = (this.age - this.ageOfLastImprovement + 1) - this.hyperParameter.penalizingAge; if (ageDept == 0) { ageDept = 1; @@ -107,7 +112,7 @@ export class Species { for (const network of this.networks) { network.sharedFitness = network.fitness; - // Penalize fitness if it has not improved for a certain amount of ages + // Penalize fitness if it has not improved for a certain number of ages if (ageDept >= 1) { network.sharedFitness = network.sharedFitness * 0.01; Container.debugLog(`Penalizing stagnant species ${this.uID}`); @@ -150,10 +155,7 @@ export class Species { // Determines how many members of this species are allowed to reproduce. // Ensure that the species will not go extinct -> at least one member survives. - let numberOfParents = Math.floor((this.hyperParameter.parentsPerSpecies * this.networks.length)); - if (numberOfParents === 0) { - numberOfParents = 1; - } + const numberOfParents = Math.floor((this.hyperParameter.parentsPerSpecies * this.networks.length)); // Allow the first to reproduce. for (const network of this.networks.slice(0, numberOfParents + 1)) { @@ -166,7 +168,7 @@ export class Species { * Those leftOvers are carried on from calculation to calculation across all species and are awarded to the * population champion's species. * The given implementation follows the approach described within the NEAT publication. - * @param leftOver makes sure to not lose childs due to rounding errors. + * @param leftOver makes sure to not lose children due to rounding errors. * @returns number leftOver collects rounding errors to ensure a constant populationSize. */ public getNumberOfOffspringsNEAT(leftOver: number): number { @@ -198,7 +200,7 @@ export class Species { * Calculates the number of offspring based on the average fitness across all members of the species. Saves * leftOvers occurring due to rounding errors and carries them on from calculation to calculation across all * species to assign them to the population champion's species in the end. - * @param leftOver leftOver makes sure to not lose childs due to rounding errors. + * @param leftOver leftOver makes sure to not lose children due to rounding errors. * @param totalAvgSpeciesFitness the average fitness of all species combined. * @param populationSize the size of the whole population. * @returns number leftOver collects rounding errors to ensure a constant populationSize. @@ -255,12 +257,9 @@ export class Species { else if (champCloned < 1) { child = this.champion.cloneStructure(true) as C; champCloned++; - } - + } else if (this._randomness.nextDouble() <= this._hyperParameter.mutationWithoutCrossover || this.networks.length == 1) { // With a user-defined probability or if the species holds only one network, we apply mutation without - // crossover. - else if (this._randomness.nextDouble() <= this._hyperParameter.mutationWithoutCrossover || - this.networks.length == 1) { + // the crossover operation. child = this.breedMutationOnly(); } @@ -309,7 +308,7 @@ export class Species { const parent1 = this._randomness.pick(this.networks); let parent2: C; - // Pick second parent either from within the species or from another species. + // Pick a second parent either from within the species or from another species. if (this._randomness.nextDouble() > this._hyperParameter.interspeciesMating || populationSpecies.length < 2) { parent2 = this._randomness.pick(this.networks); } @@ -317,11 +316,11 @@ export class Species { // Select second parent from a different species. else { const candidateSpecies = populationSpecies.filter(species => species.uID !== this.uID && species.networks.length > 0); - // Check if we have at least one other species that contains at least 1 network. + // Check if we have at least one other species that contains at least one network. if (candidateSpecies.length > 0) { parent2 = this._randomness.pick(candidateSpecies).networks[0]; } - // If we don't find another suitable species we have to mate within our species. + // If we don't find another suitable species, we have to mate within our species. else { parent2 = this._randomness.pick(this.networks); } @@ -330,7 +329,7 @@ export class Species { // Apply crossover. let child = parent1.crossover(parent2)[0]; - // We may get a defect network. Just return it restart the breeding process for this child. + // We may get a defect network. Restart the breeding process for this child. if(!child){ return undefined; } @@ -412,6 +411,14 @@ export class Species { return this._uID; } + get representative(): C { + return this._representative; + } + + set representative(value: C) { + this._representative = value; + } + get age(): number { return this._age; } diff --git a/whisker-main/src/whisker/whiskerNet/NeuroevolutionPopulations/TargetStatementPopulation.ts b/whisker-main/src/whisker/whiskerNet/NeuroevolutionPopulations/TargetStatementPopulation.ts index 0458489a..a6f7fa41 100644 --- a/whisker-main/src/whisker/whiskerNet/NeuroevolutionPopulations/TargetStatementPopulation.ts +++ b/whisker-main/src/whisker/whiskerNet/NeuroevolutionPopulations/TargetStatementPopulation.ts @@ -1,7 +1,6 @@ import {NeatPopulation} from "./NeatPopulation"; import {StatementFitnessFunction} from "../../testcase/fitness/StatementFitnessFunction"; import {NeatChromosome} from "../Networks/NeatChromosome"; -import {NetworkChromosome} from "../Networks/NetworkChromosome"; import {ChromosomeGenerator} from "../../search/ChromosomeGenerator"; import {Container} from "../../utils/Container"; import {Randomness} from "../../utils/Randomness"; @@ -9,6 +8,7 @@ import {NeatestParameter} from "../HyperParameter/NeatestParameter"; import {NeuroevolutionTestGenerationParameter} from "../HyperParameter/NeuroevolutionTestGenerationParameter"; import {ScratchEvent} from "../../testcase/events/ScratchEvent"; import {FeatureGroup, InputFeatures} from "../Misc/InputExtraction"; +import {NeatChromosomeGenerator} from "../NetworkGenerators/NeatChromosomeGenerator"; export class TargetStatementPopulation extends NeatPopulation { @@ -25,11 +25,11 @@ export class TargetStatementPopulation extends NeatPopulation { * Generates an initial population of networks by cloning and mutating networks that * proved to be viable solutions for their targeted statement. To not get stuck in certain network structures * and to favour simple network structures, we also generate some new networks. In case we have not yet covered - * anything, we just generate the desired amount of networks using the defined NetworkGenerator. + * anything, we just generate the desired number of networks using the defined NetworkGenerator. */ public override generatePopulation(): void { // If we don't have any starting networks, i.e. it's the first ever selected fitness target simply generate - // the desired amount of networks using the defined generator. + // the desired number of networks using the defined generator. if (this._startingNetworks.length === 0) { while (this.networks.length < this.populationSize) { const network = this.generator.get(); @@ -39,6 +39,10 @@ export class TargetStatementPopulation extends NeatPopulation { const discoveredInputs = this._fetchDiscoveredInputStates(); const discoveredEvents = this._fetchDiscoveredOutputEvents(); + if (this.generator instanceof NeatChromosomeGenerator) { + this.generator.inputSpace = discoveredInputs; + this.generator.outputSpace = discoveredEvents; + } // Otherwise, we start with cloning all starting networks. for (const network of this._startingNetworks) { @@ -47,7 +51,6 @@ export class TargetStatementPopulation extends NeatPopulation { break; } const clone = network.cloneStructure(true); - this._updateInOutLayer(network, discoveredInputs, discoveredEvents); this.networks.push(clone); } @@ -61,9 +64,8 @@ export class TargetStatementPopulation extends NeatPopulation { break; } const network = this.generator.get(); - this._updateInOutLayer(network, discoveredInputs, discoveredEvents); - // With the given probability apply gradient descent if enabled + // With the given probability, we apply gradient descent if enabled if (Container.backpropagationInstance && !this._switchedToEasierTarget && random.nextDouble() <= (this.hyperParameter as NeatestParameter).gradientDescentProb) { Container.backpropagationInstance.gradientDescent(network, this._targetStatementFitness.getNodeId()); @@ -73,13 +75,11 @@ export class TargetStatementPopulation extends NeatPopulation { } // The remaining networks are generated by mutating prior solutions. - // Reset the gradient descent parent flag for the new target. this._startingNetworks.forEach(network => network.gradientDescentChild = false); let i = 0; while (this.networks.length < this.hyperParameter.populationSize) { const parent = this._startingNetworks[i % this._startingNetworks.length]; const mutant = parent.mutate(); - this._updateInOutLayer(mutant, discoveredInputs, discoveredEvents); this.networks.push(mutant); i++; } @@ -124,16 +124,4 @@ export class TargetStatementPopulation extends NeatPopulation { } return [...discoveredEvents.values()]; } - - /** - * Updates a network with the provided in and output features. This is necessary as otherwise speciation fails due - * to an explosion of species. - * @param network to be updated. - * @param inputs that will be used to update the input layer. - * @param outputs that will be used to update the output layer. - */ - private _updateInOutLayer(network: NetworkChromosome, inputs: InputFeatures, outputs: ScratchEvent[]): void { - network.updateInputNodes(inputs); - network.updateOutputNodes(outputs); - } } diff --git a/whisker-main/src/whisker/whiskerNet/Operators/NeatCrossover.ts b/whisker-main/src/whisker/whiskerNet/Operators/NeatCrossover.ts index b86e0a62..e92ab163 100644 --- a/whisker-main/src/whisker/whiskerNet/Operators/NeatCrossover.ts +++ b/whisker-main/src/whisker/whiskerNet/Operators/NeatCrossover.ts @@ -1,10 +1,7 @@ import {Pair} from "../../utils/Pair"; -import {ConnectionGene} from "../NetworkComponents/ConnectionGene"; -import {NodeGene} from "../NetworkComponents/NodeGene"; import {Randomness} from "../../utils/Randomness"; import {NetworkCrossover} from "./NetworkCrossover"; import {NeatChromosome} from "../Networks/NeatChromosome"; -import {NetworkLayer} from "../Networks/NetworkChromosome"; export class NeatCrossover extends NetworkCrossover { @@ -64,145 +61,56 @@ export class NeatCrossover extends NetworkCrossover { * @param avgWeights determines whether we inherit matching genes randomly or by averaging the weight of both parent * connections. */ - private multipointCrossover(parent1: NeatChromosome, parent2: NeatChromosome, avgWeights) { + private multipointCrossover(parent1: NeatChromosome, parent2: NeatChromosome, avgWeights: boolean) { // Check which parent has the higher non-adjusted fitness value // The worst performing parent should not add additional connections // If they have the same fitness value, take the smaller ones excess and disjoint connections only - let p1Better = false; - const parent1Size = parent1.connections.length; - const parent2Size = parent2.connections.length; - - if (parent1.fitness > parent2.fitness) - p1Better = true; - else if (parent1.fitness === parent2.fitness) { - if (parent1Size < parent2Size) { - p1Better = true; - } + let fittestParent: NeatChromosome; + let lessFitParent: NeatChromosome; + if (parent1.fitness > parent2.fitness) { + fittestParent = parent1; + lessFitParent = parent2; + } else if (parent1.fitness < parent2.fitness) { + fittestParent = parent2; + lessFitParent = parent1; + } else if (parent1.connections.length < parent2.connections.length) { + fittestParent = parent1; + lessFitParent = parent2; + } else { + fittestParent = parent2; + lessFitParent = parent1; } - // Create Lists for the new Connections and the layer map that includes the new nodes. - const newConnections: ConnectionGene[] = []; - const newLayers: NetworkLayer = new Map(); - - // Iterators for the connections of both parents - let i1 = 0; - let i2 = 0; - - // Average weight, only used when the flag is activated. - let avgWeight = undefined; - - // Booleans for deciding if we inherit a connection and if we enable the new Connection. - let skip = false; - - // Here we save the chosen connection for each iteration of the while loop and if it's a recurrent one. - let currentConnection: ConnectionGene; - - while (i1 < parent1Size || i2 < parent2Size) { - - // reset the skip value and the avgWeight value - skip = false; - avgWeight = undefined; - - // Excess Genes coming from parent2 - if (i1 >= parent1Size) { - currentConnection = parent2.connections[i2]; - i2++; - // Skip excess genes from the worse parent - if (p1Better) { - skip = true; - } - } - // Excess genes coming from parent 1 - else if (i2 >= parent2Size) { - currentConnection = parent1.connections[i1]; - i1++; - // Skip excess genes from the worse parent - if (!p1Better) { - skip = true; - } - } + // The crossover child inherits all nodes and connections from the fittest parent. + const child = fittestParent.cloneStructure(true); - // Matching genes or Disjoint Genes - else { - const parent1Connection = parent1.connections[i1]; - const parent2Connection = parent2.connections[i2]; - const parent1Innovation = parent1Connection.innovation; - const parent2Innovation = parent2Connection.innovation; - - // Matching genes are chosen randomly between the parents - if (parent1Innovation === parent2Innovation) { - if (this.random.randomBoolean()) { - currentConnection = parent1Connection; - } else { - currentConnection = parent2Connection; - } + // Map innovation numbers to the corresponding connection weights of the lesser fit parent. + const lessFitParentInnovations = new Map(); + for (const connection of lessFitParent.connections) { + lessFitParentInnovations.set(connection.innovation, connection.weight); + } - if (avgWeights) { - avgWeight = (parent1Connection.weight + parent2Connection.weight) / 2.0; - } + // Iterate over all connections from the fittest parent and inherit all disjoint and excess genes. + // When faced with matching genes inherit connections randomly from any of the two parents. + for (const connection of child.connections) { - i1++; - i2++; - } + // Matching genes + if (lessFitParentInnovations.has(connection.innovation)) { + const lessFitWeight = lessFitParentInnovations.get(connection.innovation); - // Disjoint connections are inherited from the more fit parent - else if (parent1Innovation < parent2Innovation) { - currentConnection = parent1Connection; - i1++; - if (!p1Better) { - skip = true; - } + // Average weights of matching genes + if (avgWeights) { + connection.weight = (connection.weight + lessFitWeight) / 2; } else { - currentConnection = parent2Connection; - i2++; - if (p1Better) { - skip = true; - } - } - } - - // Now add the new Connection if we found a valid one. - if (!skip) { - // Check for the nodes and add them if they are not already in the new Nodes List - const sourceNode = currentConnection.source; - const targetNode = currentConnection.target; - - // Clone and add the sourceNode to the new layer map. - let newSourceNode = [...newLayers.values()].flat().find(node => node.uID === sourceNode.uID); - if (!newSourceNode) { - newSourceNode = sourceNode.clone(); - if (!newLayers.has(newSourceNode.depth)) { - newLayers.set(newSourceNode.depth, []); - } - newLayers.get(newSourceNode.depth).push(newSourceNode); - } - - // Clone and add the targetNode to the new layer map. - let newTargetNode = [...newLayers.values()].flat().find(node => node.uID === targetNode.uID); - if (!newTargetNode) { - newTargetNode = targetNode.clone(); - if (!newLayers.has(newTargetNode.depth)) { - newLayers.set(newTargetNode.depth, []); + // Pick weight of one parent randomly. + // Note that at this point the connection has already inherited the weight of the fitter parent. + if (this.random.randomBoolean()) { + connection.weight = lessFitWeight; } - newLayers.get(newTargetNode.depth).push(newTargetNode); - } - - // Now add the new Connection - const newConnection = new ConnectionGene(newSourceNode, newTargetNode, currentConnection.weight, - currentConnection.isEnabled, currentConnection.innovation); - - // Average the weight if we calculated a value for matching genes. - if (avgWeight) { - newConnection.weight = avgWeight; } - - newConnections.push(newConnection); } } - - // Finally, create the child with the selected Connections and Nodes - return new NeatChromosome(newLayers, newConnections, parent1.getMutationOperator(), - parent1.getCrossoverOperator(), parent1.inputConnectionMethod, parent1.activationFunction); + return child; } } diff --git a/whisker-main/test/whisker/whiskerNet/Algorithms/NEAT.test.ts b/whisker-main/test/whisker/whiskerNet/Algorithms/NEAT.test.ts index 12c81872..eb35ad27 100644 --- a/whisker-main/test/whisker/whiskerNet/Algorithms/NEAT.test.ts +++ b/whisker-main/test/whisker/whiskerNet/Algorithms/NEAT.test.ts @@ -4,20 +4,29 @@ import {NetworkChromosome} from "../../../../src/whisker/whiskerNet/Networks/Net import {SearchAlgorithm} from "../../../../src/whisker/search/SearchAlgorithm"; import {SearchAlgorithmProperties} from "../../../../src/whisker/search/SearchAlgorithmProperties"; import {Chromosome} from "../../../../src/whisker/search/Chromosome"; -import {FixedIterationsStoppingCondition} from "../../../../src/whisker/search/stoppingconditions/FixedIterationsStoppingCondition"; +import { + FixedIterationsStoppingCondition +} from "../../../../src/whisker/search/stoppingconditions/FixedIterationsStoppingCondition"; import {NetworkFitnessFunction} from "../../../../src/whisker/whiskerNet/NetworkFitness/NetworkFitnessFunction"; import {Randomness} from "../../../../src/whisker/utils/Randomness"; import {FitnessFunctionType} from "../../../../src/whisker/search/FitnessFunctionType"; import {WaitEvent} from "../../../../src/whisker/testcase/events/WaitEvent"; import {MouseMoveEvent} from "../../../../src/whisker/testcase/events/MouseMoveEvent"; import {KeyPressEvent} from "../../../../src/whisker/testcase/events/KeyPressEvent"; -import {NeuroevolutionTestGenerationParameter} from "../../../../src/whisker/whiskerNet/HyperParameter/NeuroevolutionTestGenerationParameter"; +import { + NeuroevolutionTestGenerationParameter +} from "../../../../src/whisker/whiskerNet/HyperParameter/NeuroevolutionTestGenerationParameter"; import {Container} from "../../../../src/whisker/utils/Container"; import {ActivationFunction} from "../../../../src/whisker/whiskerNet/NetworkComponents/ActivationFunction"; import {NeatChromosomeGenerator} from "../../../../src/whisker/whiskerNet/NetworkGenerators/NeatChromosomeGenerator"; import {NeatMutation} from "../../../../src/whisker/whiskerNet/Operators/NeatMutation"; import {NeatCrossover} from "../../../../src/whisker/whiskerNet/Operators/NeatCrossover"; import {InputFeatures} from "../../../../src/whisker/whiskerNet/Misc/InputExtraction"; +import {NeatPopulation} from "../../../../src/whisker/whiskerNet/NeuroevolutionPopulations/NeatPopulation"; +import {ScratchEvent} from "../../../../src/whisker/testcase/events/ScratchEvent"; +import {ParameterType} from "../../../../src/whisker/testcase/events/ParameterType"; +import {NeuroevolutionUtil} from "../../../../src/whisker/whiskerNet/Misc/NeuroevolutionUtil"; +import {NodeType} from "../../../../src/whisker/whiskerNet/NetworkComponents/NodeType"; export const generateInputs = (): InputFeatures => { const genInputs: InputFeatures = new Map>(); @@ -55,15 +64,15 @@ describe('Test NEAT', () => { const mutationConfig = { "operator": "neatMutation", "mutationWithoutCrossover": 0.25, - "mutationAddConnection": 0.2, - "recurrentConnection": 0.1, + "mutationAddConnection": 0.5, + "recurrentConnection": 0, "addConnectionTries": 20, - "populationChampionNumberOffspring": 10, - "populationChampionNumberClones": 5, + "populationChampionNumberOffspring": 3, + "populationChampionNumberClones": 1, "populationChampionConnectionMutation": 0.3, - "mutationAddNode": 0.1, + "mutationAddNode": 0.05, "mutateWeights": 0.6, - "perturbationPower": 2.5, + "perturbationPower": 1.5, "mutateToggleEnableConnection": 0.1, "toggleEnableConnectionTimes": 3, "mutateEnableConnection": 0.03 @@ -114,7 +123,6 @@ describe('Test NEAT', () => { }); }); - /* Exclude due to long runtime test("XOR Sanity Test", () => { const inputMap = new Map>(); inputMap.set("Test", new Map()); @@ -126,51 +134,55 @@ describe('Test NEAT', () => { const events = [new XOR()]; - const generator = new NeatChromosomeGenerator(inputMap, events, "fully", ActivationFunction.RELU, mutation, crossover); + const generator = new NeatChromosomeGenerator(inputMap, events, "fully", ActivationFunction.SIGMOID, mutation, crossover); const population = new NeatPopulation(generator, properties); population.generatePopulation(); let found = false; + let generation = 0; + let speciesString = "Current fitness Target: XOR\n"; while (!found) { + // console.log("Generation: " + generation); for (const network of population.networks) { - let fitness = 0; - network.flushNodeValues(); + let error_sum = 0; for (let i = 0; i < 2; i++) { inputMap.get("Test").set("Gate1", i); for (let k = 0; k < 2; k++) { - inputMap.get("Test").set("Gate2", k); - network.activateNetwork(inputMap); - - let output: number; - if (network.regressionNodes.get('XOR')[0].nodeValue > 1) - output = 1; - else - output = 0; - - let result: number; + let groundTruth: number; if (i === k) - result = 0; + groundTruth = 0; else - result = 1; + groundTruth = 1; + + inputMap.get("Test").set("Gate2", k); + network.activateNetwork(inputMap); - if (output === result) - fitness++; + const networkOutput = NeuroevolutionUtil.sigmoid(network.classificationNodes.get('XOR').nodeValue, 1); + error_sum += Math.abs(groundTruth - Math.abs(networkOutput)); } } - network.fitness = fitness; - if (fitness === 4) + network.fitness = (4 - error_sum) ** 2; + if (network.fitness >= 15.8) { found = true; - } - let fitness = 0; - for(const net of population.networks){ - if (net.fitness > fitness){ - fitness = net.fitness; + // console.log(network.toString()); + break; } } population.updatePopulationStatistics(); + + const sortedSpecies = population.species.sort((a, b) => b.uID - a.uID); + speciesString = speciesString.concat(`Population of ${population.populationSize} distributed in ${sortedSpecies.length} species\n`); + speciesString = speciesString.concat("\tID\tage\tsize\tfitness\n"); + for (const species of sortedSpecies) { + speciesString = speciesString.concat(`\t${species.uID}\t${species.age}\t${species.networks.length}\t${Math.round(species.averageFitness * 100) / 100}\t${species.expectedOffspring}\n`); + } + speciesString = speciesString.concat("\n"); + population.evolve(); + generation++; } - expect(population.populationChampion.fitness).toBe(4); + // console.log(speciesString); + expect(population.populationChampion.fitness).toBeGreaterThan(15.7); }); @@ -181,7 +193,7 @@ describe('Test NEAT', () => { } getSearchParameterNames(): string[] { - return ['GateInput']; + return []; } getParameters(): unknown[] { @@ -201,11 +213,13 @@ describe('Test NEAT', () => { } toJSON(): Record { - throw new Error("Method not implemented."); + const json = {}; + json['type'] = "XOR"; + return json; } numSearchParameter(): number { - return 1; + return 0; } setParameter(args: number[], argType: ParameterType): number[] { @@ -213,5 +227,5 @@ describe('Test NEAT', () => { } } - */ + }); diff --git a/whisker-main/test/whisker/whiskerNet/NetworkGenerators/NeatChromosomeGenerator.test.ts b/whisker-main/test/whisker/whiskerNet/NetworkGenerators/NeatChromosomeGenerator.test.ts index b3b69592..c3562479 100644 --- a/whisker-main/test/whisker/whiskerNet/NetworkGenerators/NeatChromosomeGenerator.test.ts +++ b/whisker-main/test/whisker/whiskerNet/NetworkGenerators/NeatChromosomeGenerator.test.ts @@ -74,7 +74,7 @@ describe('Test NeatChromosomeGenerator', () => { ActivationFunction.TANH, mutationOp, crossoverOp); const neatChromosome = generator.get(); expect(neatChromosome.getAllNodes().length).toBe(21); - expect(neatChromosome.connections.length).toBe(27); + expect(neatChromosome.connections.length).toBe(36); expect(neatChromosome.inputNodes.get("Sprite1").size).toEqual(5); expect(neatChromosome.inputNodes.get("Sprite2").size).toEqual(4); expect(neatChromosome.getAllNodes().filter(node => node instanceof HiddenNode).length).toBe(2); diff --git a/whisker-main/test/whisker/whiskerNet/Networks/NeatChromosome.test.ts b/whisker-main/test/whisker/whiskerNet/Networks/NeatChromosome.test.ts index b1491d38..13ec4674 100644 --- a/whisker-main/test/whisker/whiskerNet/Networks/NeatChromosome.test.ts +++ b/whisker-main/test/whisker/whiskerNet/Networks/NeatChromosome.test.ts @@ -22,7 +22,6 @@ import {NeatChromosomeGenerator} from "../../../../src/whisker/whiskerNet/Networ import {NetworkChromosome, NetworkLayer} from "../../../../src/whisker/whiskerNet/Networks/NetworkChromosome"; import {Randomness} from "../../../../src/whisker/utils/Randomness"; import {ActivationTrace} from "../../../../src/whisker/whiskerNet/Misc/ActivationTrace"; -import {FitnessFunction} from "../../../../src/whisker/search/FitnessFunction"; import {EventAndParameters, ExecutionTrace} from "../../../../src/whisker/testcase/ExecutionTrace"; import {InputFeatures} from "../../../../src/whisker/whiskerNet/Misc/InputExtraction"; import {generateInputs} from "../Algorithms/NEAT.test"; @@ -255,9 +254,9 @@ describe('Test NeatChromosome', () => { const inputNode = chromosome.inputNodes.get("Sprite1").get("X-Position"); const outputNode = chromosome.layers.get(1)[0]; const hiddenNode = new HiddenNode(7, 0.5, ActivationFunction.SIGMOID); - const deepHiddenNode = new HiddenNode(8, 0.5, ActivationFunction.SIGMOID); - chromosome.addNode(hiddenNode, inputNode, outputNode); - chromosome.addNode(deepHiddenNode, hiddenNode, outputNode); + const deepHiddenNode = new HiddenNode(8, 0.75, ActivationFunction.SIGMOID); + chromosome.addNode(hiddenNode); + chromosome.addNode(deepHiddenNode); chromosome.connections.push(new ConnectionGene(inputNode, hiddenNode, 0.5, true, 7)); chromosome.connections.push(new ConnectionGene(hiddenNode, outputNode, 0, true, 8)); chromosome.connections.push(new ConnectionGene(hiddenNode, deepHiddenNode, 1, true, 9)); @@ -348,7 +347,7 @@ describe('Test NeatChromosome', () => { test('Network activation with hidden layer', () => { const chromosome = getSampleNetwork(); const hiddenNode = new HiddenNode(101, 0.5, ActivationFunction.SIGMOID); - chromosome.addNode(hiddenNode, chromosome.layers.get(0)[0], chromosome.layers.get(1)[1]); + chromosome.addNode(hiddenNode); chromosome.connections.push(new ConnectionGene(chromosome.layers.get(0)[0], hiddenNode, 1.1, true, 121)); chromosome.connections.push(new ConnectionGene(chromosome.layers.get(0)[1], hiddenNode, 1.2, true, 123)); chromosome.connections.push(new ConnectionGene(hiddenNode, chromosome.layers.get(1)[0], 1.3, true, 123)); @@ -378,7 +377,7 @@ describe('Test NeatChromosome', () => { test('Network activation with recurrent connection from classification to hidden node', () => { const chromosome = getSampleNetwork(); const hiddenNode = new HiddenNode(101, 0.5, ActivationFunction.SIGMOID); - chromosome.addNode(hiddenNode, chromosome.layers.get(0)[0], chromosome.layers.get(1)[1]); + chromosome.addNode(hiddenNode); chromosome.connections.push(new ConnectionGene(chromosome.layers.get(0)[0], hiddenNode, 1.1, true, 121)); chromosome.connections.push(new ConnectionGene(chromosome.layers.get(0)[1], hiddenNode, 1.2, true, 123)); chromosome.connections.push(new ConnectionGene(hiddenNode, chromosome.layers.get(1)[0], 1.3, true, 123)); @@ -502,7 +501,7 @@ describe('Test NeatChromosome', () => { expect(chromosome.connections.length).toBeGreaterThan(oldConnectionSize); expect(NeatPopulation.nodeToId.size).toBe(oldMapSize + 5); expect(chromosome.layers.size).toEqual(3); - expect(chromosome.layers.get(0.5).length).toEqual(8); + expect(chromosome.layers.get(0.5).length).toEqual(2); expect(chromosome.layers.get(1)[chromosome.layers.get(1).length - 1].uID).toEqual( chromosome2.layers.get(1)[chromosome2.layers.get(1).length - 1].uID); expect(chromosome.layers.get(1)[chromosome.layers.get(1).length - 1].uID).not.toEqual(