📄 Language Evolution - README.txt
×

Language Evolution

LinguisticsSocial Science LineChartRenderer

This model, based on Luc Steels and Frédéric Kramer's Bootstrapping Grounded Word Semantics (1999), shows how a shared language can emerge from scratch through language games. Agents begin with no common vocabulary. Each turn, a "speaker" indicates a unique object from a set of three; a "listener" guesses the intended meaning (a distinguishing feature like "blue" or "circle") and offers a word for it. If the word matches the speaker's intention, the word-meaning association is reinforced in both agents' lexicons.

Starting from random word generation, agents gradually converge on a shared vocabulary that enables successful communication. Despite the model's simplicity, it produces emergent linguistic phenomena observed in natural languages: synonymy (multiple words for the same meaning), dialectal variation (subgroups developing their own usage), and context-sensitivity (words acquiring narrow or context-dependent meanings). The model contributes to the field of evolutionary linguistics, illustrating how cultural conventions can arise without central design.

💻 language-evolution.js - Interactive Editor
×
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 += `&nbsp;Speaker: "${word}" (${meaning})<br />&nbsp;`;
  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();

Edit the code on the left · See results on the right