Language Evolution
Starting with no common language, a group of agents learn to communicate with each other through a guessing game. Each turn, an agent points to a unique object in a group of three objects. A second agent guesses the 'meaning' behind the first agent's act of pointing — a distinguishing feature, like “blue,” “circle,” or “1st” — and produces a word for that meaning. If it matches the word the first agent had in mind, the association between the word and the meaning are strengthened (however, in ambiguous contexts, it's possible for the second agent to match the first agent's word but for a different meaning). At the start, since the agents have no vocabulary, they generate new words and use them randomly, but over time, a shared language emerges that allows them to successfully communicate about the features of objects.
Despite the simplicity of this model, a number of complex linguistic phenomena emerge, including synonyms (multiple words with the same meaning), differing usage/'slang' among sub-populations, and context-specificity (ex. a word that is associated with “blue square,” but not squares in general). This model is based on Luc Steels and Frédéric Kramer's Bootstrapping Grounded Word Semantics (1999).
import { Agent, Environment, LineChartRenderer, utils } from "flocc"; const POPULATION = 30; const MUTATION_RATE = 0.005; let SHOW_DICTIONARY = false; let VISUALIZE_GUESSING = true; let timeout = null; const colors = ["red", "blue", "green", "yellow", "black", "gray"]; const shapes = ["circle", "square", "triangle"]; const objects = colors .map(color => shapes.map(shape => ({ color, shape }))) .reduce((a, b) => a.concat(b)); const tokens = ["a", "i", "u", "e", "o"] .map(v => ["k", "g", "s", "t", "n", "h", "b", "p", "m", "r"].map(c => c + v)) .reduce((acc, cur) => acc.concat(cur)); class Lexicon { constructor() { this.associations = []; } addAssociation(word, meaning) { // don't overwrite any existing associations if ( this.associations.find(a => { return a.word === word && a.meaning === meaning; }) ) { return; } this.associations.push({ word, meaning, score: 1 }); } adjustAssociation(word, meaning, adjust) { const association = this.associations.find(a => { return a.word === word && a.meaning === meaning; }); if (!association) return this.addAssociation(word, meaning); association.score += adjust; } } const environment = new Environment(); const chart = new LineChartRenderer(environment, { autoScale: true, height: 200, width: 400, range: { max: 1.1, min: -0.1 } }); chart.metric("correct", { fn: arr => utils.sum(arr) / POPULATION }); chart.mount("#chart"); /** * Generate a random 2-3 syllable word taken from the tokens */ function randomWord() { return new Array(utils.random(2, 3)) .fill(0) .map(() => utils.sample(tokens)) .join(""); } function numToOrdinal(n) { n++; if (n === 1) return "1st"; if (n === 2) return "2nd"; if (n === 3) return "3rd"; return n + "th"; } /** * Given a "topic" (an object with a color, shape, and position) * in a "context" (a group of objects), generate the most likely "meaning" * -- either the color, shape, or position of the topic */ function meaningFromTopicAndContext(topic, context) { let meaning; const numSameColor = context.filter(obj => obj.color === topic.color).length - 1; const numSameShape = context.filter(obj => obj.shape === topic.shape).length - 1; if (numSameColor > 0 && numSameShape > 0) { // if there are more than one of the same color and of the same shape, // meaning is the topic's position in the context meaning = numToOrdinal(context.indexOf(topic)); } else { if (numSameColor === 0 && numSameShape === 0) { // meaning is totally ambiguous, randomly pick one // with bias toward the position in list meaning = utils.sample( [numToOrdinal(context.indexOf(topic)), topic.color, topic.shape], [2, 1, 1] ); } else if (numSameColor === 0) { meaning = topic.color; } else { meaning = topic.shape; } } return meaning; } /** * Given a "meaning" (i.e. "triangle," "blue," or 0, 1, 2, ...) * retrieve a word if a good candidate exists, or generate a new one */ function wordFromMeaning(agent, meaning) { const { lexicon } = agent.getData(); const associations = lexicon.associations.filter(a => a.meaning === meaning); let word; if (associations.length === 0) { word = randomWord(); lexicon.addAssociation(word, meaning); } else { let bestGuess = -Infinity; let bestWords = []; associations.forEach(a => { if (a.score > bestGuess) { bestGuess = a.score; bestWords = [a.word]; } else if (a.score === bestGuess) { bestWords.push(a.word); } }); if (bestWords.length === 0 || Math.random() < MUTATION_RATE) { word = randomWord(); lexicon.addAssociation(word, meaning); } else { word = utils.sample(bestWords); } } return word; } function wordFromContext(agent, topic, context) { const meaning = meaningFromTopicAndContext(topic, context); const word = wordFromMeaning(agent, meaning); return [word, meaning]; } function tick(agent) { let guesser; do { guesser = utils.sample(environment.getAgents()); } while (guesser === agent); // select an object const context = utils.shuffle(objects).slice(0, 3); const topic = utils.sample(context); const [word, meaning] = wordFromContext(agent, topic, context); const [guess, guessMeaning] = wordFromContext(guesser, topic, context); agent.set("context", context); agent.set("topic", topic); agent.set("word", word); agent.set("meaning", meaning); agent.set("guess", guess); agent.set("guessMeaning", guessMeaning); const { lexicon } = guesser.getData(); if (word === guess) { agent.set("correct", 1); lexicon.adjustAssociation(guess, guessMeaning, 1); } else if (word !== guess) { agent.set("correct", 0); lexicon.adjustAssociation(guess, guessMeaning, -1); lexicon.adjustAssociation(word, guessMeaning, 1); } } function setup() { for (let i = 0; i < POPULATION; i++) { const agent = new Agent({ lexicon: new Lexicon() }); agent.addRule(tick); environment.addAgent(agent); } } function renderDictionary(container) { container.innerHTML = ""; const table2 = document.createElement("table"); const dictionary = []; const allLexicons = []; environment.getAgents().forEach(agent => { const { lexicon } = agent.getData(); lexicon.associations.forEach(a => { if (!dictionary.includes(a.word)) dictionary.push(a.word); allLexicons.push(a); }); }); // console.log("dictionary", dictionary.length); const allAssociations = dictionary.map(word => { const associations = allLexicons.filter(a => a.word === word); const meanings = [...new Set(associations.map(a => a.meaning))]; const scores = []; meanings.forEach((meaning, i) => { scores[i] = associations .filter(a => a.meaning === meaning) .reduce((a, b) => a + b.score, 0); }); return { word, meanings, scores }; }); allAssociations.sort((a, b) => utils.max(b.scores) - utils.max(a.scores)); allAssociations.slice(0, 20).forEach(a => { const row = document.createElement("tr"); row.innerHTML += `<td>${a.word}</td><td>${a.meanings.join("\n")}</td><td>${a.scores.join("\n")}</td>`; table2.appendChild(row); }); container.appendChild(table2); } function draw(container, object, isSelected) { const background = object.shape === "triangle" ? "transparent" : object.color; container.innerHTML += `<span style="color: ${ object.color }; background-color: ${background}" class="shape ${ isSelected ? "shape--selected" : "" } ${object.shape}"></span>`; } function visualize( container, { topic, context, word, meaning, guess, guessMeaning } ) { container.innerHTML = ""; context.forEach((obj, i) => draw(container, obj, obj === topic)); const div = document.createElement("div"); div.innerHTML += ` Speaker: "${word}" (${meaning})<br /> `; container.appendChild(div); setTimeout(() => { div.innerHTML += `Guesser: "${guess}" (${guessMeaning})`; timeout = null; }, 1000); } function run() { environment.tick(); if (SHOW_DICTIONARY) renderDictionary(document.getElementById("container")); if (VISUALIZE_GUESSING) { visualize( document.getElementById("guessing"), environment.getAgents()[0].getData() ); timeout = setTimeout(run, 2500); } else { if (environment.time < 1000) requestAnimationFrame(run); } } document.getElementById("toggle-dictionary").addEventListener("change", () => { SHOW_DICTIONARY = document.getElementById("toggle-dictionary").checked; if (!SHOW_DICTIONARY) document.getElementById("container").innerHTML = ""; if (environment.time >= 1000) run(); }); document.getElementById("visualize-guessing").addEventListener("change", () => { VISUALIZE_GUESSING = document.getElementById("visualize-guessing").checked; if (!VISUALIZE_GUESSING) document.getElementById("guessing").innerHTML = ""; if (timeout) { clearTimeout(timeout); run(); } }); setup(); run();