Skip to content

Commit

Permalink
Merge branch 'fix-recurrent-inputNode-connections' into 'master'
Browse files Browse the repository at this point in the history
Multiple Improvements for Neatest

See merge request se2/whisker/whisker-main!373
  • Loading branch information
FeldiPat committed Oct 10, 2023
2 parents b03d9ea + c303f14 commit 2e8421e
Show file tree
Hide file tree
Showing 22 changed files with 358 additions and 338 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}
```
14 changes: 7 additions & 7 deletions config/Neuroevolution/neatest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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,
Expand All @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions config/Neuroevolution/neatestBackprop.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"testGenerator": "neuroevolution",
"algorithm": "neatest",
"eventSelection": "activation",
"populationSize": 100,
"populationSize": 150,
"numberOfSpecies": 5,
"parentsPerSpecies": 0.20,
"penalizingAge": 15,
Expand All @@ -21,7 +21,7 @@
"epochs": 1000,
"batchSize": 1,
"labelSmoothing": 0,
"probability": 1,
"probability": 0.6,
"peerToPeerSharing": false,

"dataAugmentation": {
Expand All @@ -42,7 +42,7 @@
"mutationWithoutCrossover": 0.25,
"mutationAddConnection": 0.05,
"recurrentConnection": 0.1,
"addConnectionTries": 50,
"addConnectionTries": 10,
"populationChampionNumberOffspring": 3,
"populationChampionNumberClones": 1,
"populationChampionConnectionMutation": 0.3,
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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());
}
Expand Down
2 changes: 1 addition & 1 deletion whisker-main/src/whisker/testcase/ScratchEventExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
36 changes: 25 additions & 11 deletions whisker-main/src/whisker/testcase/events/MouseMoveToEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,40 +24,42 @@ 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<void> {
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<string, any> {
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 {
return 0;
}

getParameters(): [number, number] {
return [this.x, this.y];
return [this._x, this._y];
}

getSearchParameterNames(): [] {
Expand All @@ -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;
}
}
32 changes: 16 additions & 16 deletions whisker-main/src/whisker/whiskerNet/Algorithms/Neatest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>();

/**
* Holds a record of promising targets, i.e. the maximum amount of how often a target has accidentally already been
Expand All @@ -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<Map<number, NeatChromosome>> {
this.initialise();
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand All @@ -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));
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}

/**
Expand All @@ -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");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
5 changes: 3 additions & 2 deletions whisker-main/src/whisker/whiskerNet/Misc/NetworkExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
25 changes: 21 additions & 4 deletions whisker-main/src/whisker/whiskerNet/Misc/StateActionRecorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]);
}
Expand Down Expand Up @@ -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<string, number>;
switch (event.toJSON()['type']) {
case "WaitEvent":
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,14 @@ export class ReliableStatementFitness implements NetworkFitnessFunction<NetworkC
await ReliableStatementFitness.updateUncoveredMap(network);
executor.resetState();

// Stop if we failed to cover our target statement.
// If the chromosome did not manage to reach the target statement, add the inverted distance toward the
// target statement to the fitness function.
if(!await network.targetFitness.isCovered(network)){
network.fitness += (1 / await network.targetFitness.getFitness(network));
break;
continue;
}

// At this point we know that we have covered the statement again.
// At this point, we know that we have covered the statement again.
// If Peer-To-Peer Sharing is activated, add collected state-action trace to gradient descent ground truth data.
if (Container.backpropagationInstance && Container.peerToPeerSharing) {
this._peerToPeerSharing(network);
Expand Down
Loading

0 comments on commit 2e8421e

Please sign in to comment.