Redirect Chapter 32: NLP Projects & Applications | AI Fundamentals
← Back to Tutorials Chapter 32

NLP Projects & Applications

The Projects in This Chapter

The previous chapter built the vocabulary of text representation. Now we use it. Every project here follows the same skeleton: load text data, clean it, convert it to numbers with a representation, train a classifier, and measure the result. Once you can repeat that skeleton, the specific dataset hardly matters.

Memory hook: The pipeline is the skill. Spam, reviews, and sentiment are just different coats of paint on the same engine.

Spam vs Ham: The Dataset

The classic starter dataset is a collection of SMS or email messages labelled spam (unwanted) or ham (legitimate). Each message is a short document, which makes it perfect for trying representations quickly. Load the data and inspect the class balance first:

import pandas as pd

df = pd.read_csv("messages.csv")
print(df["label"].value_counts())
print(df.head())

An imbalanced set needs handling, so always check the distribution before training.

Spam vs Ham with Bag of Words

The first model uses a bag of words and a Naive Bayes or logistic regression classifier. The plan:

  1. Split the messages into train and test sets.
  2. Build a CountVectorizer on the training text only.
  3. Transform both train and test text to count matrices.
  4. Train a classifier and score it on the test set.
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import accuracy_score, confusion_matrix

X_train, X_test, y_train, y_test = train_test_split(
    df["message"], df["label"], test_size=0.2, random_state=42)

vectorizer = CountVectorizer(stop_words="english")
X_train_vec = vectorizer.fit_transform(X_train)
X_test_vec = vectorizer.transform(X_test)

model = MultinomialNB()
model.fit(X_train_vec, y_train)
preds = model.predict(X_test_vec)
print(accuracy_score(y_test, preds))
print(confusion_matrix(y_test, preds))

BOW plus Naive Bayes is a classic pair for spam because spam messages share heavy word overlap.

Spam vs Ham with TF-IDF

The same pipeline with TfidfVectorizer often performs better, because it de-emphasises words like "the" and "you" that appear everywhere and rewards the distinctive terms spammers use:

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression

tfidf = TfidfVectorizer(stop_words="english", ngram_range=(1, 2))
X_train_vec = tfidf.fit_transform(X_train)
X_test_vec = tfidf.transform(X_test)

model = LogisticRegression(max_iter=1000)
model.fit(X_train_vec, y_train)
preds = model.predict(X_test_vec)
print(accuracy_score(y_test, preds))

Adding bigrams lets the model catch phrases, and comparing the two representations on the same split tells you which suits your data.

Heads-up: Never call fit on the test text. The vectorizer must be fitted on the training set alone and only transformed on test data, otherwise the model's score is misleadingly optimistic.

Best Practices for Solving ML Problems

Across these projects a handful of habits separate reliable results from lucky ones:

Text Classification with Word2Vec and Average Word2Vec

Counting representations work, but word embeddings capture meaning. This project classifies messages by converting them to averaged Word2Vec vectors.

Part 1: Preparing the Averaged Vectors

Train or load a Word2Vec model, then write a helper that averages the vectors of every word in a message. Messages with no known words become a zero vector:

import numpy as np
from gensim.models import Word2Vec

sentences = [msg.split() for msg in df["message"]]
w2v = Word2Vec(sentences, vector_size=100, window=5,
               min_count=1, sg=1, epochs=10)

def avg_vector(text):
    tokens = text.split()
    vectors = [w2v.wv[w] for w in tokens if w in w2v.wv]
    if not vectors:
        return np.zeros(w2v.vector_size)
    return np.mean(vectors, axis=0)

X = np.array([avg_vector(msg) for msg in df["message"]])

Part 2: Training and Evaluating

Once each message is a fixed-size vector, any normal classifier applies:

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, df["label"], test_size=0.2, random_state=42)

model = RandomForestClassifier(random_state=42)
model.fit(X_train, y_train)
print(model.score(X_test, y_test))

Averaging loses word order, but it is fast and often competitive with TF-IDF, while giving you vectors you can reuse for clustering and similarity.

Kindle Review Sentiment Analysis

The Kindle review dataset contains book reviews with star ratings. Converting ratings into positive and negative sentiment gives a natural classification task: reviews with four or five stars are positive, one or two stars are negative, and three stars are dropped to keep the signal clear.

Part 1: Loading and Exploring

import pandas as pd

reviews = pd.read_csv("kindle_reviews.csv")
reviews["label"] = reviews["rating"].apply(
    lambda r: "pos" if r >= 4 else ("neg" if r <= 2 else None)
)
reviews = reviews.dropna(subset=["label"])
print(reviews["label"].value_counts())
print(reviews["review"][0])

Exploration should also look at review lengths and a few samples per class, so you know what the model is actually reading.

Part 2: Modeling and Evaluation

Reuse the pipeline: split, vectorize with TF-IDF (bigrams help sentiment), train, and report a confusion matrix alongside accuracy:

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    reviews["review"], reviews["label"],
    test_size=0.2, random_state=42)

vec = TfidfVectorizer(stop_words="english", ngram_range=(1, 2))
X_tr = vec.fit_transform(X_train)
X_te = vec.transform(X_test)

model = LogisticRegression(max_iter=1000)
model.fit(X_tr, y_train)
print(classification_report(y_test, model.predict(X_te)))

Use classification_report so you see precision and recall for both classes. A model that says "positive" for everything would score decently on accuracy but uselessly on negative reviews, and the report exposes exactly that.

Heads-up: Reviews are noisier than SMS spam. Typos, slang, and emojis are common, so a little cleaning, lowercasing, and punctuation removal usually buys a measurable accuracy gain before you train anything.

Choosing the Right Representation

You now have three representations and can reason about when to reach for each:

None is universally best; the winning choice depends on your dataset. That is why every project in this chapter evaluates the same split with each representation and lets the scores decide.

Tip: Keep a shared helper file with your split, cleaning, and scoring functions. Reusing the same evaluation harness is what makes comparison honest.
Exercise: Run the spam vs ham project twice, once with bag of words and once with TF-IDF, on the same split, and record both scores. Then run the Kindle sentiment project and print the classification report. Finally, take the ten messages your best model misclassified, read them, and write one sentence about why each was wrong. This last step is where real learning happens.