All articles
May 15, 20255 min read

How a bag of word-pairs judges a movie

NLPWord2VecInterpretabilitySentiment Analysis

Most portfolio ML projects show you a confusion matrix and call it a day. I wanted the opposite: a model you can poke. So I put my IMDB movie-sentiment classifier on the web — it runs entirely in your browser, no server — and made it show its work.

Try the live demo →

It's about 88% accurate at calling a review positive or negative. That number is the boring part. The interesting part is how it decides, and exactly where it falls apart.

A bag of word-pairs

The model is deliberately old-school — and that's what makes it a good teacher. The whole pipeline:

  1. Clean & tokenise the review (lowercase, strip punctuation, lemmatise).
  2. Form bigrams — overlapping word-pairs joined with an underscore, so "not good" becomes the single token not_good.
  3. Look up a vector for each bigram. These come from Word2Vec (skip-gram, 200 dimensions) trained on 50,000 reviews, so word-pairs that appear in similar contexts end up with similar vectors.
  4. Average all the bigram vectors into one 200-number summary of the review.
  5. Classify that average with a tiny 2-layer neural network → positive or negative.

That's it. No attention, no recurrence — just average the word-pairs and draw a line. The demo shows every step live, colouring each bigram by how positive or negative it leans on its own.

Cleaning the labels first

Before training, I ran a noise-robust pass over the data: an Isolation Forest for anomaly detection plus TextBlob polarity as a sanity check, to catch reviews whose label probably disagrees with their text. About 1,500 of the 50,000 (roughly 3%) got flagged and removed. A small, honest gain — cleaner labels, a slightly steadier model — and a reminder that data quality is usually cheaper than model complexity.

What it gets right

Bigrams buy you something real: local negation. Because "not good" is stored as the single token not_good — distinct from "good" — the model learns it's negative. Type "Not good." into the demo and it nails it. A plain bag-of-words couldn't.

Where it breaks (the fun part)

The trouble starts the moment meaning stretches beyond two adjacent words:

  • "This film is not bad." → the model says negative. To a human, "not bad" is mild praise. Averaged bigrams can't reassemble that.
  • "I wouldn't say it was bad."negative again. The "not" is too far from "bad" to ever land in the same pair.
  • "boring but ultimately rewarding" vs "rewarding but ultimately boring" → swap the words and the verdict flips, but in the shallow, wrong direction. The model has no idea that "but ultimately X" means X wins.

None of this is a bug. It's the direct, visible consequence of averaging word-pairs and throwing away order.

The lesson

This is exactly the wall that transformers knocked down. Attention lets every word weigh every other, so a "not" can reach across a whole sentence to flip "a bad film", and word order finally carries meaning. The jump from this 2013-era bag-of-embeddings to a modern LLM is, in large part, the jump from averaging to attention.

I keep this model around not because it's state of the art — it obviously isn't — but because its failures are legible. You can see precisely why it's wrong, which is the best possible intuition for why the models I actually build today are shaped the way they are.

Poke at it