Sentiment Analyzer: Classifying Real Text at Scale

Data Science & ML Projects (Intermediate)

Chapter 1 · Sentiment Analyzer: Classifying Real Text at Scale

A social-media/email tone-and-sentiment analyzer was the very first project idea raised when this entire Data Science & ML subject was first agreed — long before nlp1 existed to make it possible. This chapter delivers on that promise directly, by taking nlp1-10's own exact capstone pipeline — unchanged — and pointing it at real data for the first time: 25,000 real IMDb movie reviews, not a handful of illustrative sentences.

What We're Building

The identical architecture nlp1-10 already built and proved works: preprocessing (nlp1-1) → frozen pretrained GloVe embeddings (nlp1-9) → an LSTM sequence classifier (nlp1-6). Nothing about the model changes here — what changes is scale: a real, freely available, canonical sentiment dataset instead of a toy example, and everything that comes with training on tens of thousands of real examples instead of a handful.

Step 1: Loading the Real Dataset

import pandas as pd

df = pd.read_csv("imdb_reviews.csv")   # columns: review, sentiment ("positive"/"negative")
print(df.shape)
print(df["sentiment"].value_counts())

The IMDb movie review dataset is the canonical benchmark for exactly this task — 25,000 real reviews, roughly balanced between positive and negative, freely available and genuinely unremarkable to work with (no scraping, no missing values, no cleaning pipeline needed here — that work belongs to Chapter 2's own dataset, not this one).

Step 2: Preprocessing — nlp1-1's Own Pipeline, Reused Directly

import re

def preprocess(text):
    text = text.lower()
    text = re.sub(r"[^a-z\s]", "", text)
    return text.split()

df["tokens"] = df["review"].apply(preprocess)

nlp1-1's own tokenization/cleaning approach, applied unchanged — nothing here needed to be redesigned for real data, since preprocessing was always meant to generalize beyond whatever toy examples originally demonstrated it.

Step 3: Building a Real Vocabulary

from collections import Counter

all_tokens = [tok for tokens in df["tokens"] for tok in tokens]
vocab = {word: i + 2 for i, (word, count) in enumerate(Counter(all_tokens).most_common(20000))}
vocab["<pad>"] = 0
vocab["<unk>"] = 1
print(f"Vocabulary size: {len(vocab)}")
A real-scale detail nlp1's own toy examples never needed
A real dataset's own raw vocabulary can easily exceed 100,000 distinct words. Capping it at the 20,000 most frequent (most_common(20000)) keeps the embedding table a manageable size — a genuinely practical necessity at this scale that a five-sentence toy example never has to face.

Step 4: Loading Only the GloVe Vectors This Vocabulary Actually Needs

import numpy as np

embedding_dim = 100
embedding_matrix = np.zeros((len(vocab), embedding_dim))

with open("glove.6B.100d.txt", encoding="utf-8") as f:
    for line in f:
        parts = line.split()
        word = parts[0]
        if word in vocab:
            embedding_matrix[vocab[word]] = np.array(parts[1:], dtype="float32")

The full GloVe file (nlp1-9's own pretrained embeddings) contains vectors for roughly 400,000 words — loading every single one into memory just to use 20,000 of them would be genuine, avoidable waste. This loop reads the file once and keeps only the rows this specific vocabulary needs.

Step 5: The Exact Model From nlp1-6/nlp1-10 — Unchanged

import torch
import torch.nn as nn

class SentimentClassifier(nn.Module):
    def __init__(self, embedding_matrix, hidden_dim=128):
        super().__init__()
        self.embedding = nn.Embedding.from_pretrained(
            torch.tensor(embedding_matrix, dtype=torch.float32), freeze=True
        )
        self.lstm = nn.LSTM(embedding_matrix.shape[1], hidden_dim, batch_first=True)
        self.output = nn.Linear(hidden_dim, 1)

    def forward(self, x):
        embedded = self.embedding(x)
        _, (hidden, _) = self.lstm(embedded)
        return torch.sigmoid(self.output(hidden[-1]))
Not a rewrite — the same architecture, at real scale
This is nlp1-10's own capstone model, character for character. The frozen embedding choice, the LSTM reading only earlier context, the single sigmoid output — none of it needed to change to handle real data. What genuinely changes in the next two steps is everything around the model: how much data flows through it, and how long training actually takes.

Step 6: Training — Now With Real Batching

from torch.utils.data import Dataset, DataLoader

def encode(tokens, max_len=200):
    ids = [vocab.get(t, 1) for t in tokens[:max_len]]
    return ids + [0] * (max_len - len(ids))

class ReviewDataset(Dataset):
    def __init__(self, df):
        self.X = [encode(t) for t in df["tokens"]]
        self.y = (df["sentiment"] == "positive").astype("float32").values

    def __len__(self):
        return len(self.X)

    def __getitem__(self, i):
        return torch.tensor(self.X[i]), torch.tensor(self.y[i])

loader = DataLoader(ReviewDataset(df), batch_size=64, shuffle=True)

model = SentimentClassifier(embedding_matrix)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
loss_fn = nn.BCELoss()

for epoch in range(3):
    for X_batch, y_batch in loader:
        optimizer.zero_grad()
        preds = model(X_batch).squeeze()
        loss = loss_fn(preds, y_batch)
        loss.backward()
        optimizer.step()
    print(f"Epoch {epoch+1} done.")
A genuine scale gotcha nlp1's own toy examples never hit
25,000 reviews can't all pass through the model at once — DataLoader splits them into batches of 64, and fixed-length encoding (max_len=200, padding shorter reviews and truncating longer ones) is what makes batching possible at all, since a batch needs every sequence in it to be the same length. Training also genuinely takes real time here — minutes on a modern CPU, much less on a GPU — unlike nlp1's own instant toy examples. This is the honest cost of real scale, not a sign anything is wrong.

Step 7: Evaluating on Real Reviews

sample_reviews = [
    "This movie was absolutely fantastic, best film I've seen all year.",
    "Waste of two hours. Terrible acting, worse plot.",
]
for review in sample_reviews:
    encoded = torch.tensor([encode(preprocess(review))])
    score = model(encoded).item()
    print(f"{score:.2f} — {review[:50]}...")

A genuinely satisfying moment this project earns that a toy example can't: real, freshly-written sentences, never seen during training, scored by a model trained on real human-written reviews rather than a handful of illustrative examples.

The same nlp1-10 architecture

Frozen GloVe + LSTM + sigmoid — nothing about the model changed for real data.

Vocabulary capping

20,000 most-frequent words, not the full raw vocabulary — a real necessity at scale.

Loading only needed embeddings

One pass through the GloVe file, keeping only rows this vocabulary needs.

Batching & padding

Fixed-length encoding makes real-scale training via DataLoader possible.

Extend This Project

Try these on your own:

  • Swap the frozen embeddings for fine-tuned ones (freeze=False, per nlp1-9's own guidance) now that there's enough real data to justify it — compare accuracy.
  • Save the trained model and vocabulary to disk so this project doesn't need retraining every time it's reused elsewhere in this course.
  • Try the same pipeline on a genuinely different kind of text — real tweets or product reviews — and see whether accuracy holds up on a different writing style.
  • Extend Step 7 into a small function that accepts any string and returns a plain "positive"/"negative" label instead of a raw score.

What's Next

Chapter 2: Customer Churn Predictor — the full ml1 depth Chapter 7 of the Beginner course deliberately declined to use: real feature engineering, a genuine model comparison, cross-validation, and the full precision/recall/F1 picture.

Chapter 1 Quick Reference

  • Delivers directly on this subject's own original roadmap promise — a real sentiment analyzer, first raised before nlp1 even existed
  • Reuses nlp1-10's own exact capstone architecture unchanged — frozen GloVe embeddings + LSTM + sigmoid
  • What genuinely changes at real scale: vocabulary capping, loading only needed embeddings, and batching/padding via DataLoader
  • Training now takes real time (minutes, not instant) — an honest cost of real scale, not a sign of a problem
  • Evaluated on genuinely new, never-seen sentences — not just held-out examples from the same training set