Natural Language Processing

NLP gives computers the ability to understand, generate, and reason about human language. It underpins search engines, chatbots, translation systems, and modern LLMs. This section covers the foundational building blocks: turning words into vectors, measuring word association, and building simple language models.

Word Embeddings

Represent words as dense numerical vectors capturing semantic meaning

Word Association

Measure how strongly two words co-occur using PMI and bigrams

Language Models

Predict the next word given a context window (n-gram and neural)

Word2Vec

Word2Vec learns dense word embeddings from raw text. The core idea: "a word is known by the company it keeps." Words that appear in similar contexts get similar vectors.

The skip-gram model predicts context words given a target word. Training adjusts the embedding matrix so that similar words end up close together in vector space.

NumPy Implementation (Skip-gram)

import numpy as np

class Word2Vec:
    def __init__(self, vocab_size, embed_dim=10, lr=0.01):
        self.w1 = np.random.randn(vocab_size, embed_dim) * 0.01
        self.w2 = np.random.randn(embed_dim, vocab_size) * 0.01
        self.lr = lr

    def softmax(self, x):
        e = np.exp(x - x.max())
        return e / e.sum()

    def forward(self, x):
        h = x @ self.w1          # hidden layer (embedding lookup)
        u = h @ self.w2           # output scores
        return self.softmax(u), h

    def train_step(self, x, y_true):
        y_pred, h = self.forward(x)
        error = y_pred - y_true   # output error
        # Backprop
        dw2 = np.outer(h, error)
        dw1 = np.outer(x, error @ self.w2.T)
        self.w1 -= self.lr * dw1
        self.w2 -= self.lr * dw2
        return -np.log(y_pred[y_true.argmax()] + 1e-9)  # cross-entropy loss

Using Gensim (Production)

from gensim.models import Word2Vec
from nltk.tokenize import word_tokenize

corpus = [word_tokenize(s.lower()) for s in sentences]

model = Word2Vec(corpus, vector_size=100, window=5,
                 min_count=1, workers=4, epochs=10)

# Find most similar words
print(model.wv.most_similar('machine', topn=5))

# Word arithmetic: king - man + woman ≈ queen
result = model.wv.most_similar(
    positive=['king', 'woman'], negative=['man'], topn=1
)
print(result)

PMI & Word Collocations

Pointwise Mutual Information (PMI) measures how much more often two words co-occur than if they were statistically independent. High PMI = strong association.

// PMI Formula

PMI(x, y) = log[ P(x, y) / (P(x) × P(y)) ]

from nltk.collocations import BigramCollocationFinder, BigramAssocMeasures
from nltk.tokenize import word_tokenize

text = """natural language processing and machine learning are
          subfields of artificial intelligence and machine learning
          overlaps with natural language processing significantly"""

tokens = word_tokenize(text.lower())
finder = BigramCollocationFinder.from_words(tokens)

# Score all bigrams by PMI
measures = BigramAssocMeasures()
for bigram, score in finder.score_ngrams(measures.pmi):
    print(f"{bigram[0]} + {bigram[1]:<20} PMI = {score:.2f}")

Collocations with high PMI — like "machine learning" or "natural language" — are phrases that carry meaning together. Knowing this is crucial for tokenization and phrase detection.

Cosine Similarity Explorer

Drag the sliders to set two user interest vectors across 4 topics. Watch how cosine similarity changes — it measures the angle between vectors, not their magnitude.

Preset:

User A

User B

Interest Profile

Sports vs Music Projection

Cosine Similarity

0.72

Language Models

A language model assigns a probability to a sequence of words. Given previous words, it predicts what comes next — the foundation behind autocomplete, GPT, and text generation.

Trigram Language Model

from collections import defaultdict
import random

def build_trigram_model(corpus):
    model = defaultdict(lambda: defaultdict(float))
    for sentence in corpus:
        words = sentence.split()
        for i in range(len(words) - 2):
            w1, w2, w3 = words[i], words[i+1], words[i+2]
            model[(w1, w2)][w3] += 1

    # Convert counts to probabilities
    for context, nexts in model.items():
        total = sum(nexts.values())
        for w in nexts:
            nexts[w] /= total
    return model

def generate_text(model, seed, n=20):
    words = seed.split()
    for _ in range(n):
        context = (words[-2], words[-1])
        next_words = model.get(context)
        if not next_words:
            break
        words.append(random.choices(
            list(next_words.keys()),
            weights=list(next_words.values())
        )[0])
    return ' '.join(words)

model = build_trigram_model(reuters_sentences)
print(generate_text(model, seed="the stock market"))

Why Trigrams Fall Short

Limitations of N-gram Models

  • Sparsity — most trigrams never appear in training data
  • Fixed context — can't capture dependencies beyond n words
  • No generalisation — "cat sat" and "dog sat" have completely separate entries

Neural language models (LSTMs, Transformers) solve all three — they generalise across vocabulary and capture arbitrarily long context via attention.

Knowledge Check

Test your understanding — pick the best answer, then click Check.

1. What does Word2Vec learn?

2. What is a bigram?

3. Why is PMI useful for finding collocations?