diff --git a/src/app/evolve.config.js b/src/app/evolve.config.js index b5837da72a48ced790cfc5b2cbcefea2735b00d2..96fb0b52b375639fadf0584b6cc8e2c73f2106e4 100644 --- a/src/app/evolve.config.js +++ b/src/app/evolve.config.js @@ -5,11 +5,14 @@ const root = path.resolve(__dirname); module.exports = { target: 'node', - entry: path.resolve(root, 'evolve.ts'), + entry: { + evolve: path.resolve(root, 'evolve/index.ts'), + 'evolve.worker': path.resolve(root, 'evolve/worker.ts') + }, mode: dev ? 'development' : 'production', output: { path: path.resolve(root, '..', 'dist'), - filename: 'evolve.js' + filename: '[name].js' }, resolve: { extensions: ['.ts', '.js'] diff --git a/src/app/evolve.ts b/src/app/evolve.ts deleted file mode 100644 index f53a461df39235354e17cd373a5f122407f159a9..0000000000000000000000000000000000000000 --- a/src/app/evolve.ts +++ /dev/null @@ -1,118 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -import loader from '@assemblyscript/loader'; - -import { AyakoAS } from '../ai'; -import { DummyActuator, DummyInputManager, DummyStorageManager } from '../dummies'; -import { GameManager } from '../game_manager'; - -import { BENCHMARK_SEED } from './shared'; -import { RNG } from './rng'; - -const POPULATION_SIZE = 64; -const GENE_MIN = 0; -const GENE_MAX = 5; -const MUTATE_PRECISION = 100; -const MUTATE_STRENGTH = 0.6; -const MUTATE_LIKELY = 0.6; - -const rng = new RNG(Math.random) - -type Genes = [number, number, number, number, number, number, number, number, number]; - -var generation = 1; - -function blank(): Genes { - return [0, 0, 0, 0, 0, 0, 0, 0, 0]; -} - -function mutateGene(gene: number): number { - return Math.max(GENE_MIN, Math.min(GENE_MAX, Math.floor((gene + rng.normal() * MUTATE_STRENGTH / Math.log(generation + 2)) * MUTATE_PRECISION) / MUTATE_PRECISION)); -} - -function mutateIndividual(genes: Genes): Genes { - const result = blank(); - for (var i = 0; i < genes.length; i++) result[i] = (Math.random() > MUTATE_LIKELY) ? mutateGene(genes[i]) : genes[i]; - return result; -} - -const gameManager = new GameManager(4, new DummyInputManager, new DummyActuator, new DummyStorageManager, - () => BENCHMARK_SEED); -var ayako = new AyakoAS(gameManager, new DummyInputManager, - loader.instantiateSync(fs.readFileSync(path.join(__dirname, '../../static/dist/ayako.rel.wasm')))); - -const baseGenes: Genes = [ - +ayako.module.exports.WEIGHT_CLEANLY, - +ayako.module.exports.WEIGHT_GREEDY, - +ayako.module.exports.WEIGHT_CORNER, - +ayako.module.exports.WEIGHT_MONO3, - +ayako.module.exports.WEIGHT_CORNER_REWARD, - +ayako.module.exports.WEIGHT_CORNER_PENALTY, - +ayako.module.exports.WEIGHT_MONO3_REWARD, - +ayako.module.exports.WEIGHT_MONO3_PENALTY, - +ayako.module.exports.WEIGHT_IMPATIENCE -]; -const GENE_COUNT = baseGenes.length; - -var population: Genes[] = []; - -function mean(...m: number[]): number { - return m.reduce((x, y) => x + y) / m.length; -} - -function breed(p0: Genes, p1: Genes): Genes { - const offspring = blank(); - for (var i = 0; i < GENE_COUNT; i++) offspring[i] = mean(p0[i], p1[i]); - return offspring; -} - -function populate(template: Genes) { - for (var i = 0; i < POPULATION_SIZE; i++) - for (var j = 0; j < GENE_COUNT; j++) - population[i][j] = template[j]; -} - -function evaluateFitness(genes: Genes): number { - ayako.reset(); - ayako.module.exports.loadWeights(...genes); - return ayako.module.exports.simulateToEnd(7); -} - -function selectFittest(): [Genes, Genes] { - var fittest: Genes = null; - var runnerUp: Genes = null; - var best = 0; - for (var i = 0; i < POPULATION_SIZE; i++) { - const candidate = population[i]; - console.log('evaluating fitness of', generation, i, '//', ...candidate); - const fitness = evaluateFitness(candidate); - if (fitness > best) { - runnerUp = fittest; - fittest = candidate; - } - console.log('score =', fitness); - } - return [fittest, runnerUp]; -} - -function doGeneration() { - console.log('mutating...'); - for (var i = 0; i < POPULATION_SIZE; i++) population[i] = mutateIndividual(population[i]); - const fittest = selectFittest(); - console.log('fittest', fittest[0]); - console.log('runner up', fittest[1]); - console.log('populating...'); - const template = breed(...fittest); - populate(template); -} - -for (var i = 0; i < POPULATION_SIZE; i++) population[i] = blank(); -populate(baseGenes); -console.log(population); -while (true) { - console.log('begin generation', generation); - doGeneration(); - console.log('end of generation', generation); - generation++; -} \ No newline at end of file diff --git a/src/app/evolve/index.ts b/src/app/evolve/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..367a3b92c927781ae025214f2fd92a7d5446cc9e --- /dev/null +++ b/src/app/evolve/index.ts @@ -0,0 +1,115 @@ +import fs from 'fs'; +import path from 'path'; + +import loader from '@assemblyscript/loader'; + +import { AyakoASCore } from '../../ai'; + +import { RNG } from './rng'; + +import { Genes } from './types'; +import { EvolutionWorkerPool } from './worker'; + +const POPULATION_SIZE = 128; +const GENE_MIN = 0; +const GENE_MAX = 5; +const MUTATE_PRECISION = 100; +const MUTATE_STRENGTH = 0.6; +const MUTATE_LIKELY = 0.7; + +const rng = new RNG(Math.random) +const pool = new EvolutionWorkerPool(7); +const ayako = loader.instantiateSync<AyakoASCore>(fs.readFileSync(path.join(__dirname, '../../static/dist/ayako.rel.wasm'))); +const baseGenes: Genes = [ + +ayako.exports.WEIGHT_CLEANLY, + +ayako.exports.WEIGHT_GREEDY, + +ayako.exports.WEIGHT_CORNER, + +ayako.exports.WEIGHT_MONO3, + +ayako.exports.WEIGHT_CORNER_REWARD, + +ayako.exports.WEIGHT_CORNER_PENALTY, + +ayako.exports.WEIGHT_MONO3_REWARD, + +ayako.exports.WEIGHT_MONO3_PENALTY, + +ayako.exports.WEIGHT_IMPATIENCE +]; +const GENE_COUNT = baseGenes.length; + +var population: Genes[] = []; +var generation = 1; + +function blankGenes(): Genes { + return [0, 0, 0, 0, 0, 0, 0, 0, 0]; +} + +function randomGenes(): Genes { + const v = blankGenes(); + for (var i = 0; i < POPULATION_SIZE; i++)v[i] = rng.uniform() * GENE_MAX; + return v; +} + +function mutateGene(gene: number): number { + return Math.max(GENE_MIN, Math.min(GENE_MAX, Math.floor((gene + rng.normal() * MUTATE_STRENGTH / Math.log(generation + 2)) * MUTATE_PRECISION) / MUTATE_PRECISION)); +} + +function mutateIndividual(genes: Genes): Genes { + const result = blankGenes(); + for (var i = 0; i < genes.length; i++) result[i] = rng.uniform() > MUTATE_LIKELY ? mutateGene(genes[i]) : genes[i]; + return result; +} + +function weightedMean(d: number, r: number, m: number): number { + return (d * m + r) / (2 + m); +} + +function breed(p0: Genes, p1: Genes): Genes { + const offspring = blankGenes(); + for (var i = 0; i < GENE_COUNT; i++) offspring[i] = weightedMean(p0[i], p1[i], 2); + return offspring; +} + +function populate(template: Genes) { + for (var i = 0; i < POPULATION_SIZE; i++) + for (var j = 0; j < GENE_COUNT; j++) + population[i][j] = template[j]; +} + +async function selectFittest(): Promise<[[Genes, number], [Genes, number]]> { + var fittest: Genes = null; + var runnerUp: Genes = null; + var best = 0, secondBest = 0; + const scores = await pool.evaluateMany(population); + console.log('evaluation complete'); + for (var i = 0; i < POPULATION_SIZE; i++) { + if (scores[i] > best) { + runnerUp = fittest; + fittest = population[i]; + secondBest = best; + best = scores[i]; + } + } + return [[fittest, best], [runnerUp, secondBest]]; +} + +async function doGeneration() { + console.log('mutating...'); + for (var i = 0; i < POPULATION_SIZE; i++) population[i] = mutateIndividual(population[i]); + console.log('evaluating fittest...'); + const results = await selectFittest(); + const fittest = results.map(x => x[0]) as [Genes, Genes]; + const scores = results.map(x => x[1]); + console.log('fittest', fittest[0].join(','), scores[0]); + console.log('runner up', fittest[1].join(','), scores[1]); + console.log('populating...'); + const template = breed(...fittest); + populate(template); +} + +(async () => { + for (var i = 0; i < POPULATION_SIZE; i++) population[i] = blankGenes(); + populate(baseGenes) + while (true) { + console.log('begin generation', generation); + await doGeneration(); + console.log('end of generation', generation); + generation++; + } +})(); \ No newline at end of file diff --git a/src/app/rng.js b/src/app/evolve/rng.js similarity index 100% rename from src/app/rng.js rename to src/app/evolve/rng.js diff --git a/src/app/evolve/types.ts b/src/app/evolve/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..1735864f6bc561c2e3b9559e80fd457d03578e0a --- /dev/null +++ b/src/app/evolve/types.ts @@ -0,0 +1 @@ +export type Genes = [number, number, number, number, number, number, number, number, number]; \ No newline at end of file diff --git a/src/app/evolve/worker.ts b/src/app/evolve/worker.ts new file mode 100644 index 0000000000000000000000000000000000000000..d6f1327a3581bedac4b7724385fa09586bcb9304 --- /dev/null +++ b/src/app/evolve/worker.ts @@ -0,0 +1,80 @@ +import fs from 'fs'; +import path from 'path'; +import { Worker, isMainThread, parentPort } from 'worker_threads'; + +import loader from '@assemblyscript/loader'; + +import { AyakoAS } from '../../ai'; +import { DummyActuator, DummyInputManager, DummyStorageManager } from '../../dummies'; +import { GameManager } from '../../game_manager'; + +import { BENCHMARK_SEED } from '../shared'; + +import { Genes } from './types'; + +const gameManager = new GameManager(4, new DummyInputManager, new DummyActuator, new DummyStorageManager, + () => BENCHMARK_SEED); +var ayako = new AyakoAS(gameManager, new DummyInputManager, + loader.instantiateSync(fs.readFileSync(path.join(__dirname, '../../static/dist/ayako.rel.wasm')))); + +function evaluateFitness(genes: Genes): number { + ayako.reset(); + ayako.module.exports.loadWeights(...genes); + return ayako.module.exports.simulateToEnd(7); +} + +if (!isMainThread) parentPort.on('message', (message: Genes) => { + const score = evaluateFitness(message); + parentPort.postMessage(score); +}); + +export class EvolutionWorker { + + busy: boolean = false; + private worker: Worker; + private waiters: ((value?: any) => void)[] = []; + + constructor() { + this.worker = new Worker(path.resolve(__dirname, 'evolve.worker.js')); + } + + private setFree() { + if (this.waiters.length > 0) this.waiters.shift()(); + else this.busy = false; + } + + waitForFree(): Promise<void> { + if (this.busy) return new Promise(resolve => this.waiters.push(resolve)); + } + + async evaluateFitnessAsync(genes: Genes): Promise<number> { + if (this.busy) await this.waitForFree(); + this.busy = true; + return await new Promise(resolve => { + this.worker.once('message', (score: number) => { + console.debug(genes.join(','), score); + this.setFree(); + resolve(score); + }); + this.worker.postMessage(genes); + }); + } + +} + +export class EvolutionWorkerPool { + + private pool: EvolutionWorker[] = []; + + constructor(private size: number) { + for (var i = 0; i < size; i++) this.pool[i] = new EvolutionWorker(); + } + + evaluateMany(population: Genes[]): Promise<number[]> { + population = population.concat(); + var results: Promise<number>[] = []; + for (var i = 0; i < population.length; i++) results.push(this.pool[i % this.size].evaluateFitnessAsync(population[i])); + return Promise.all(results); + } + +} \ No newline at end of file diff --git a/src/app/shared.ts b/src/app/shared.ts index f8329ddf01cd67c5031027d22fed120ffcd852b9..cde13a773683493c72ee2a9fe46181da89aeee42 100644 --- a/src/app/shared.ts +++ b/src/app/shared.ts @@ -1 +1 @@ -export const BENCHMARK_SEED = '0fTAPVve'; \ No newline at end of file +export const BENCHMARK_SEED = 'oUojMV5B'; \ No newline at end of file