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.
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:
- Clean & tokenise the review (lowercase, strip punctuation, lemmatise).
- Form bigrams — overlapping word-pairs joined with an underscore, so "not good" becomes the single token not_good.
- 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.
- Average all the bigram vectors into one 200-number summary of the review.
- 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
- Live demo — classify your own review, watch the word-pairs light up, and try to break it.
- Code: Movie_Sentiment_Analysis