Saturday, October 10, 2026

Bag of Words model

What is and why make a “Bag of Words” model?

Like everyone, I’ve become interested in the epistemic nature of Large Language Models (LLM).

So, how to proceed? When you focus directly at LLMs they are complicated and difficult to grok.

I’ve opted for an evolutionary approach. I think of this as stepping stones to the LLM revolution. What significant artefacts came before? And then how, who, what, why and when did improvements or leaps occur from those earlier tries to use computers to understand and imitate language, semantic language?

There is excellent information out there on the internet. Step by step I can uncover the path. As well as history, this involves concepts, code (python code) and maths.

First cab off the rank is “Bag of Words” (BOW) or properly speaking the Bag of Words model.

Bag of Words means take all the words from a document and count them. The order of the words is ignored. This turns out to be useful, more about that below.

I’ve imitated and adapted a BOW model for a Dr Seuss book:

Let’s suppose our documents have a small vocabulary. For instance, Dr. Seuss’ book Green Eggs and Ham has only fifty unique words. In alphabetical order, they are: a, am, and, anywhere, are, be, boat, box, car, could, dark, do, eat, eggs, fox, goat, good, green, ham, here, house, I, if, in, let, like, may, me, mouse, not, on, or, rain, Sam, say, see, so, thank, that, the, them, there, they, train, tree, try, will, with, would and you.

If we treat each page of the book as a single document, we can embed each of them as a 50-dimensional vector. Consider the page that reads:

I would not like them here or there.

I would not like them anywhere.

I do not like green eggs and ham.

I do not like them, Sam-I-am.

Summary of the process of making a BOW model:

  • preprocessing: tokenize the sentences, removing punctuation and unnecessary spaces
  • tokenize the words
  • count the word frequencies
  • filter out stop words if required (not done for the Dr Seuss case since not really necessary)
  • build the BOW model: a binary matrix where each row corresponds to a sentence and each column represents one of the top N frequent words
  • visualise the BOW model, in this case I made a heatmap. Other options are a frequency graph and a word cloud.
PYTHON KNOWLEDGE

One of my current goals is to improve my python programming. So, I’ll document that as well. Usually I still feel like a python beginner, but am slowly, very slowly improving. My current goal is to become more fluent with list and dictionary comprehensions, lambda and a few others (RE, heapq, sorting, zip). I have learnt some new things about tokenization, REs, numpy, matplotlib and seaborn, the latter two for visualisation. But, I do need to learn more about seaborn since I’ve yet to figure out how to rotate the xticklabels and yticklabels.

Still not happy with my python coding skills but as I said, slowly improving.

CODE

Here’s my python code, with comments for the BOW model. This particular code has been adapted from a couple of helpful sources, which are acknowledged in the doc string. I’ve left out the ordered frequency graph and Word Cloud. They are useful features but the BOW model is the main game.

# -*- coding: utf-8 -*-
"""
Created on Thu Oct  8 09:43:12 2026

@author: billk
Dr Seuss story
https://builtin.com/machine-learning/bag-of-words
modified to create BOW visual with the help of
https://www.geeksforgeeks.org/nlp/bag-of-words-bow-model-in-nlp/

"""
import re   # regular expressions
import nltk # natural language tool kit
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt


# Assumes that 'doc' is a list of strings and 'vocab' is some iterable of vocab
# words (e.g., a list or set)
def get_bag_of_words(doc, vocab):
   # Create initial dictionary which maps each vocabulary word to a count of 0
   word_count_dict = dict.fromkeys(vocab, 0)
   # For each word in the doc, increment its count
   for word in doc:
       word_count_dict[word] += 1
   # Now, initialize a vector to a list of zeros
   bag = [0] * len(vocab)
   # For every vocab word, set its index equal to its count
   for i, word in enumerate(vocab):
       bag[i] = word_count_dict[word]
   return bag

# Define the vocabulary of the whole document
vocab = ['a', 'am', 'and', 'anywhere', 'are', 'be', 'boat', 'box', 'car',\
        'could', 'dark', 'do', 'eat', 'eggs', 'fox', 'goat', 'good', 'green',\
        'ham', 'here', 'house', 'i', 'if', 'in', 'let', 'like', 'may', 'me',\
        'mouse', 'not', 'on', 'or', 'rain', 'sam', 'say', 'see', 'so', 'thank',\
        'that', 'the', 'them', 'there', 'they', 'train', 'tree', 'try', 'will',\
        'with', 'would', 'you']
# Define one page of the document
doc = ("I would not like them here or there.\n"
      "I would not like them anywhere.\n"
      "I do not like green eggs and ham.\n"
      "I do not like them, Sam-I-am.")

# convert the doc string to list of sentences
dataset = nltk.sent_tokenize(doc) 
#print(dataset) check it's working

for i in range(len(dataset)):
    dataset[i] = dataset[i].lower()  # eliminate capitals
    dataset[i] = re.sub(r'\W', ' ', dataset[i]) # eliminate non letters
    dataset[i] = re.sub(r'\s+', ' ', dataset[i]) # eliminate extra spaces

# run next two lines to check it's working
# for i, sentence in enumerate(dataset):
#    print(f"Sentence {i+1}: {sentence}") 

# initialise and make a word2count dictionary
word2count = {} 

for data in dataset:
    words = nltk.word_tokenize(data) # tokenize each sentence
    # count the words
    for word in words:
        if word not in word2count:
            word2count[word] = 1 # add dictionary item
        else:
            word2count[word] += 1 # count the words

print(word2count) # print the dictionary {'word' : num}

# create BOW matrix graph
BOW = []

for data in dataset:
    vector = []
    for word in word2count:
        if word in nltk.word_tokenize(data):
            vector.append(1)
        else:
            vector.append(0)
    BOW.append(vector)

# BOW is a list of lists
BOW = np.asarray(BOW) # convert to numpy array

# make a heatmap with seaborn
plt.figure(figsize=(10, 6))
sns.heatmap(BOW, cmap='RdYlGn', cbar=False, annot=True, fmt="d", 
            xticklabels=word2count, yticklabels=[f"Sentence {i+1}" 
            for i in range(len(dataset))])

plt.title('Bag of Words Matrix')
plt.xlabel('Frequent Words', rotation=0, ha="right") 
plt.ylabel('Sentences',rotation=0, ha="right")
plt.tight_layout()
plt.show()

Dr Seuss Bag of Words heat map (click to see more clearly):

USEFULNESS

This article summarises the surprising (given its simplicity) uses of BOW:

Although BoW is a very simple technique that turns documents into vectors it can be used by some machine learning algorithms for following use cases:

1. Plagiarism detection

If two documents produce the same or very similar BoW vectors, they likely contain the same words in similar proportions.

2. Spam detection

Emails containing words like “free”, “win”, “lottery” in high frequency can be flagged.Simple ML algorithms trained on BoW vectors still perform well for this use case.

3. Document classification

For example, news categories. Sports articles will have high counts for words like “team”, “win”, “match”. Politics articles will have high counts for words like “election”, “policy”, “minister”.

Issues arising from this research, to be elaborated on in future blogs:

  • Other more efficient pathways: scikit-learn, there is always an improved way, TF-IDF to cull out trivial words
  • Language classification algorithms, such as the naive Bayes classifier; how do they work? See the third reference below.
REFERENCE
https://builtin.com/machine-learning/bag-of-words
Provides the code, which I adapted, for the Dr Seuss Green Eggs and Ham

https://www.geeksforgeeks.org/nlp/bag-of-words-bow-model-in-nlp/
Provides NLTK code for Bag of Words plus a few visualisation techniques (Frequency table and Word Cloud)

naive Bayes classifiers link
https://scikit-learn.org/stable/modules/naive_bayes.html
In spite of their apparently over-simplified assumptions, naive Bayes classifiers have worked quite well in many real-world situations, famously document classification and spam filtering. They require a small amount of training data to estimate the necessary parameters
NLTK book
https://www.nltk.org/book/
Comprehensive account (concepts and python code) of Natural Language Processing (NLP) up until roughly 2010. Source of the NLTK python library.

Sunday, July 26, 2026

Game of Life: colour, seeds, references

A Gosper gun which is formed from 2 queen bee shuttles back to back. The debris from the bee hive sparks creates a glider every cycle of 30 generations.

The Conway life appspot site has an extensive pattern library, with hundreds of interesting seeds, which are run with changing colours.

I couldn’t figure out how to do the changing colours so I asked on Stack Exchange and within hours had received a solution. It turns out that the changing colours at the Conway appspot site are based on the age of the cells:

  • green: new cells
  • yellow: one generation old
  • orange: two generations old
  • red: three generations old

The code below shows how and includes the Gosper gun seed.

I found an article by Paul Rendell about how to make a Turing Machine in the Cellular Automaton Conway’s Game of Life!

I also found a lexicon which explains all Game of Life terminology as well as many patterns with grids.

I spent some time making some of the patterns used to make the Turing Machine but eventually gave up because it was too much for me. By the way you can watch videos of the GOL Turing Machine here.

Golly is an open source, cross-platform application for exploring Conway's Game of Life and many other types of cellular automata: John von Neumann's 29-state CA, Wolfram's 1D rules, WireWorld, Generations, Paterson's Worms, Larger than Life, etc.

CODE, just with comments about the colour update (for full comments see previous post)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.colors import ListedColormap

N = 80 # array size
universe = np.zeros((N, N), dtype = int)

s= N//2 - 20 

# Gosper glider gun
universe[s, s+24] = 1
universe[s+1,[s+22,s+24]] = 1
universe[s+2,[s+12,s+13,s+20,s+21,s+34,s+35]] = 1
universe[s+3,[s+11,s+15,s+20,s+21,s+34,s+35]] = 1
universe[s+4,[s+0,s+1,s+10,s+16,s+20,s+21]] = 1
universe[s+5,[s+0, s+1, s+10,s+14,s+16,s+17,s+22,s+24]] = 1
universe[s+6,[s+10,s+16,s+24]] = 1
universe[s+7,[s+11,s+15]] = 1
universe[s+8,[s+12,s+13]] = 1

generations = 0
# initalise age
age = np.zeros(universe.shape)
colors = ['white', 'green', 'yellow', 'orange', 'red']
discrete_cmap = ListedColormap(colors)

def animate(frame, universe, img):
    global generations, age
    new_u = np.zeros((N, N), dtype = int)

    universe[0,:] = universe[-1,:] = universe[:,0] = universe[:,-1] = 0
    new_u[1:-1,1:-1] = (universe[ :-2, :-2] + universe[ :-2,1:-1] + universe[ :-2,2:] +
                        universe[1:-1, :-2]                + universe[1:-1,2:] +
                        universe[2:  , :-2] + universe[2:  ,1:-1] + universe[2:  ,2:])

    birth = (new_u==3)[1:-1,1:-1] & (universe[1:-1,1:-1]==0)
    survive = ((new_u==2) | (new_u==3))[1:-1,1:-1] & (universe[1:-1,1:-1]==1)
    universe[:] = np.zeros((N, N), dtype = int)
    universe[1:-1,1:-1][birth | survive] = 1
    population = np.count_nonzero(universe)
    
    age += universe # monitor age of cells
    age *= universe # remove dead cells
    age[age>4] = 4 # cap the age at 4
    img.set_data(age)
    ax.set_xlabel('generations = {}, population = {}'.format(generations, population))
    generations += 1


fig = plt.figure(figsize=(5, 5))
ax = plt.axes()
ax.set_yticklabels([])
ax.set_xticklabels([])
img = ax.imshow(universe, vmin=0, vmax=4, cmap=discrete_cmap)
ani = FuncAnimation(fig, animate, fargs=(universe, img,),
                    frames=220, interval=200)
ani.save("gosperGliderGun_colour.gif") # save the animations as a gif
plt.show()

Sunday, July 19, 2026

coding Game of Life

Code is at the bottom, with extensive comments

Notes on how to code Game of Life (GOL) including an animation, using numpy and matplotlib.

I learn through doing inspirational projects and Conway’s Game of Life cellular automata is certainly that. More about that in a later blog.

I’m also learning NumPy, as a step in learning the maths behind neural nets, so initially I’m following Nicolas P. Rougier’s online book “From Python to Numpy” (2017), Ch 4.2, where he has both a python and numpy version of Game of Life. The numpy version uses vectorization, rather than for loops.

The GOL rules are:

  • Survival: if an existing alive cell has 2 or 3 neighbours then it survives.
  • Birth: if an empty cell has 3 neighbours then it becomes alive.
  • All other neighbourhood counts lead to dead cells

Each cell has 8 neighbouring cells. By slicing the universe grid for each neighbouring cell and adding the values we obtain a neighbourhood count (new_u) for each cell. Then the rules can be implemented using boolean masks which reference both the new_u neighbourhood count grids and the universe grids.

Rougier doesn’t do the animation in his book. He stops after showing how to compute neighbours and does one iteration. So I found a version on the web by finxter using python for loops and FuncAnimation

The code works but the finxter explanation is cursory.

It is essential to make a new copy of the universe. Not doing this held my up for a while. Part of the problem was that I didn’t fully understand matplotlib’s FuncAnimation so I researched that for a bit.

Here is a good explanation:
The FuncAnimation class allows us to create an animation by passing a function that iteratively modifies the data of a plot. This is achieved by using the setter methods on various Artist (examples: Line2D, PathCollection, etc.). A usual FuncAnimation object takes a Figure that we want to animate and a function func that modifies the data plotted on the figure. It uses the frames parameter to determine the length of the animation. The interval parameter is used to determine time in milliseconds between drawing of two frames.
- source

Which setter method, plotting method and Artist do I need?

  • data set method: set_data()
  • plotting method: Axes.imshow()
  • Artist: image.AxesImage
FuncAnimation looks like this:

class matplotlib.animation.FuncAnimation(fig, func, frames=None, init_func=None, fargs=None, save_count=None, *, cache_frame_data=True, **kwargs)
  • makes an animation by repeatedly calling a function func
  • You must store the created Animation in a variable (called ani in my code) that lives as long as the animation should run. Otherwise, the Animation object will be garbage-collected and the animation stops.
  • func callable: The function to call at each frame. The first argument will be the next value in frames.
  • frames: Source of data to pass func and each frame of the animation
  • fargs: Additional arguments to pass to each call to func.
  • interval: delay between frames in milliseconds
  • repeat: bool, default True

Here is how to generate the animation gif which I used for this blog:
ani.save("GOL_random.gif")

Then I worked out how to count the population of each frame and the number of generations of the game and added that to the xlabel inside the animation function

Finally, I noticed that one Conway site had an extensive pattern library with hundreds of interesting seeds which can be run WITH DYNAMICALLY CHANGING COLOURS. I looked around at how to achieve this but so far haven’t worked it out.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
"""
Game of Life
"""
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

def create_universe(N=50, p=0.5):
    return np.random.choice([0, 1], size=(N, N), p=[1-p, p])
# generates random 1 and 0s with p probability in a N*N 2D array
N = 50 # array size

universe = create_universe(N=N, p=0.5)
# universe determines image

generations = 0 # measure the number of generations

# animation function
def animate(frame, universe, img):
    global generations
    new_u = np.zeros((N, N), dtype = int)
    # new_u initialises as same size as universe, array of all 0s 
    # then calculates neighbour count
    
    # set edges of universe to zero
    # to avoid edge calculation complications
    universe[0,:] = universe[-1,:] = universe[:,0] = universe[:,-1] = 0
    # universe is 2D array with 1s and 0s
    
    # count the neighbours of the universe cells (by grid displacement)
    # new_u is neighbour counts
    new_u[1:-1,1:-1] = (universe[ :-2, :-2] + universe[ :-2,1:-1] + universe[ :-2,2:] +
                     universe[1:-1, :-2]                + universe[1:-1,2:] +
                     universe[2:  , :-2] + universe[2:  ,1:-1] + universe[2:  ,2:])

    # boolean masks to implement GOL rules
    birth = (new_u==3)[1:-1,1:-1] & (universe[1:-1,1:-1]==0) 
    # 3 neighbours for an empty cell -> birth
    survive = ((new_u==2) | (new_u==3))[1:-1,1:-1] & (universe[1:-1,1:-1]==1)
    # 2 or 3 neighbours in occupied cell -> survive
    
    # reset universe to zeros, make a new copy, then update 1s & 0s
    universe[:] = np.zeros((N, N), dtype = int) # new copy essential
    universe[1:-1,1:-1][birth | survive] = 1
    # count the population
    population = np.count_nonzero(universe)
    # update image with the data set method
    img.set_data(universe)
    # display generations and population
    ax.set_xlabel('generations = {}, population = {}'.format(generations, population))
    generations += 1 # update generations

fig = plt.figure(figsize=(5, 5)) # figure size
ax = plt.axes()
ax.set_yticklabels([]) # turn off axis ticks (but keep x axis label above)
ax.set_xticklabels([])
img = ax.imshow(universe, interpolation='nearest')
# imshow is the plotting method for AxesImage artist
ani = FuncAnimation(fig, animate, fargs=(universe, img,),
                    frames=200, interval=200)
# ani lives while animation runs, otherwise garbage collected
# fargs: arguments to pass to each call of the function 
# interval in milliseconds
ani.save("GOL_random.gif") # save the animations as a gif
plt.show()

Thursday, January 01, 2026

Books 2026

Books, videos (and long articles) I am reading / watching in 2026:

Ananthaswamy, Anil. Why Machines Learn: The Elegant Math Behind Modern AI (2024)
Banks, Iain M. Excession (1996)
Backman, Fredrik. A Man Called Ove (2014)
Bennett, Max. A Brief History of Intelligence: Why the Evolution of the Brain Holds the Key to the Future of AI (2023)
Bhattacharya, Ananyo. The Man from the Future: The Visionary Life of John van Neumann (2021)
Bird Steven, Klein Ewan, Loper Edward. Natural Language Processing with Python: Analyzing Text with the Natural Language Toolkit
Boldizar, Alexander. The Man who saw Seconds (2024)
Brooks, Rodney. Predictions Scorecard, 2026 January 01
Brooks, Rodney. Why Today’s Humanoids Won’t Learn Dexterity (September 2025)
Brooks, Rodney. Intelligence without Reason (1991)
Brooks, Rodney. Elephants Don't Play Chess (1990)
Curry, Judith. Climate Uncertainty and Risk (2023)
Dennett, Daniel. Intuition Pumps and other Tools for Thinking (2013)
Dennett, Daniel. From Bacteria to Bach and Back: The Evolution of Minds (2017)
Disher, Garry. The Divine Wind (1998)
Eisenberg, David. SVG Essentials. (2002)
Epstein, Alex. Fossil Future (2022)
Goldstein, Rebecca. The Mattering Instinct (2016)
Haverbeke, Marijn. Eloquent JavaScript (Fourth Edition). (2024)
Holson, Benjie. Benjie's Humanoid Olympic Games (September 2025)
Lawhon, Ariel. The Frozen River (2024)
Larson, Erik. The Myth Of AI: Why Computers can't think the way we do (2021)
Larson, Erik. Benderland (2026)
Lee, Tim & Trott, Sean. A jargon-free explanation of how AI large language models work (2023)
Mearsheimer, John J. The Darkness Ahead: Where The Ukraine War Is Headed (June, 2023)
Mesbahi, Farzed. Abundance or Collapse (2026)
McKinney, Wes. Python for Data Analysis, 3E (2022-23)
North, Claire. Slow Gods (2025)
Pfarrer, Chuck and Brewer, Alan. Indications and Warnings | Day 1418 | Putin's Dubious Milestone (Jan 14, 2026)
Raeini, Mohammad Ghaseminejad. The evolution of language models: From N-Grams to LLMs, and beyond (2025)
Ramalho, Luciano. Fluent Python: Clear, Concise and Effective Progamming, second edition (2022)
Robinson, Kim Stanley. Blue Mars (2009)
Rowson, Jonathan. Understanding the Grünfeld (1998)
Rougier, Nicolas P. From Python to Numpy (2017)
Schoch, Richard. The Secrets of Happiness (2006)
Seth, Anil. Being You: A New Science of Consciousness (2021)
Stewart, James. Calculus Fourth Edition (1999)
Stanley, Kenneth; Lehman, Joel. Why Greatness cannot be Planned: The Myth of the Objective (2015)
Akarsh Kumar, Jeff Clune, Joel Lehman, Kenneth O. Stanley. Questioning Representational Optimism in Deep Learning: The Fractured Entangled Representation Hypothesis (2025)
Tchaikovsky, Adrian. Children of Time (2015)
Tchaikovsky, Adrian. Children of Ruin (2019)
Tchaikovsky, Adrian. Children of Memory (2023)
Tchaikovsky, Adrian. Children of Strife (2026)
Tchaikovsky, Adrian. Shards of Earth (2021)
Torres, Jose. Sting Like a Bee: The Muhammad Ali Story (1971)
Trask, Andrew. Grokking Deep Learning (2019)
Whitehead, Colson. Underground Railroad (2016)
Yann LeCun, Yoshua Bengio, Geoffrey Hinton. Deep Learning (2015)
Zahavy,Tom. LLMs can't Jump (2026)
CODING
Plotly Open Source Graphing Library for Python
Di Méo, Grace. Creating Interactive Visualizations with Plotly
kaizenkode, Python Programming Projects for Beginners (2026)
seaborn
pandas
matplotlib
NumPy: the absolute basics for beginners
Python NumPy For Your Grandma by Ben Gorman
NumPy tutorials
Machine Learning Crash Course
GAME OF LIFE
LifeWiki
The Game of Life
Conway's Game of Life
A Universal Turing Machine in Conway’s Game of Life
Golly

Previous: Books and articles 2025