SRM Day 3 Hub | Prev Session | Next Session


Embeddings & Language Models


Seeking the soul of words. The mathematical world of language, from Word2Vec to Language Models and Neural Networks.

Word Embeddings · Language Models · Neural Networks · Attention

Word Embeddings

Giving Words a Soul

Transforming discrete words into continuous, dense, multi-dimensional floating-point vectors (Digital DNA).

  • The Semantic Topology: In this high-dimensional space, words with similar meanings (e.g., King, Prince) naturally gravitate toward each other.
  • Unlike the rigid accountant that is BoW, Embeddings act like a poet—capturing the nuances, emotions, and multifaceted relationships of human language.

Word2Vec

Training the Semantic Map

Rule: *"You shall know a word by the company it keeps."* (J.R. Firth, popularized by Mikolov in 2013)

  • Architecture 1: CBOW (Continuous Bag of Words): Predicts a hidden target word based on its surrounding context words. (Fast and efficient).
  • Architecture 2: Skip-Gram: Takes a single target word and predicts the surrounding context words. (Slower, but superior for rare words).

The Magic of Word2Vec

Algebraic Semantics

vector("King") - vector("Man") + vector("Woman") ≈ vector("Queen")

  • Real-world Impact:
    1. Semantic Search: Querying "Cheap Laptop" retrieves "Budget Notebook".
    2. Cross-Lingual Embeddings: Mapping French and English vector spaces for zero-shot translation.
    3. Recommendation Systems (Item2Vec): "Users who bought this also bought..."

💻 Code: Vector Arithmetic

(Understanding word relationships using Numpy arrays)


import numpy as np

# Hypothetical multi-dimensional word embeddings
# Dimensions: [Royalty_Score, Masculinity_Score]
vec_king   = np.array([0.9,  0.9])
vec_man    = np.array([0.0,  0.9])
vec_woman  = np.array([0.0, -0.9])

# The Word2Vec Magic equation
vec_result = vec_king - vec_man + vec_woman

print("Computed Vector (King - Man + Woman):")
print(vec_result)

# Ideal vector for Queen = [Royalty, Femininity]
vec_queen = np.array([0.9, -0.9])

print("\nTarget Vector (Queen):")
print(vec_queen)

print("\n💡 The algebraic operation perfectly arrives at the semantic concept of 'Queen'!")
                        

💻 Code: Cosine Similarity

(The mathematical backbone of semantic search and NLP distance metrics)


import numpy as np
from numpy.linalg import norm

def cosine_similarity(vec1, vec2):
    """Calculates cosine of angle between vectors. Range: -1 to 1"""
    return np.dot(vec1, vec2) / (norm(vec1) * norm(vec2))

# Hypothetical embeddings
vec_king   = np.array([0.9,  0.8,  0.1])
vec_queen  = np.array([0.9, -0.8,  0.1])
vec_man    = np.array([0.1,  0.8,  0.0])
vec_apple  = np.array([0.0,  0.0,  0.9])

print("Cosine Similarity Scores:")
print("-" * 30)
print(f"King  & Queen : {cosine_similarity(vec_king, vec_queen):.3f}")
print(f"King  & Man   : {cosine_similarity(vec_king, vec_man):.3f}")
print(f"King  & Apple : {cosine_similarity(vec_king, vec_apple):.3f}")

print("\nConclusion: King and Queen share high semantic alignment.")
print("King and Apple are completely orthogonal (unrelated).")
                        

💻 Hands-On: Bag of Words (BoW)

(Convert text to numerical vectors by counting word frequencies)


from sklearn.feature_extraction.text import CountVectorizer
import numpy as np

# Sample sentences
sentences = [
    "I love machine learning",
    "Machine learning is amazing",
    "I love deep learning too"
]

# Create Bag of Words vectorizer
vectorizer = CountVectorizer()
bow_matrix = vectorizer.fit_transform(sentences)

# Get vocabulary
vocab = vectorizer.get_feature_names_out()

print("📚 Bag of Words Representation")
print("="*50)
print(f"\nVocabulary: {list(vocab)}\n")

print("BoW Matrix (rows=documents, cols=words):")
print("-"*50)
for i, sent in enumerate(sentences):
    counts = bow_matrix[i].toarray()[0]
    print(f"\nDoc {i+1}: '{sent}'")
    print(f"Vector: {counts}")
    # Show non-zero words
    words = [(vocab[j], counts[j]) for j in range(len(counts)) if counts[j] > 0]
    print(f"Words:  {dict(words)}")

print("\n💡 Limitation: 'I love ML' and 'ML I love'")
print("   have IDENTICAL vectors (no word order)!")
                        

💻 Hands-On: Find Similar Words

(Explore a mini embedding space interactively)


import numpy as np
from numpy.linalg import norm

# Mini word embedding space (simplified 4D vectors)
# Dimensions: [royalty, gender(M+/F-), animal, technology]
word_vectors = {
    "king":     np.array([0.9,  0.8, 0.0, 0.0]),
    "queen":    np.array([0.9, -0.8, 0.0, 0.0]),
    "prince":   np.array([0.7,  0.6, 0.0, 0.0]),
    "princess": np.array([0.7, -0.6, 0.0, 0.0]),
    "man":      np.array([0.1,  0.9, 0.0, 0.0]),
    "woman":    np.array([0.1, -0.9, 0.0, 0.0]),
    "dog":      np.array([0.0,  0.2, 0.9, 0.0]),
    "cat":      np.array([0.0, -0.2, 0.9, 0.0]),
    "computer": np.array([0.0,  0.0, 0.0, 0.9]),
    "laptop":   np.array([0.0,  0.0, 0.0, 0.85]),
}

def cosine_sim(v1, v2):
    return np.dot(v1, v2) / (norm(v1) * norm(v2))

def find_similar(word, top_n=3):
    """Find most similar words to a given word"""
    if word not in word_vectors:
        return []
    vec = word_vectors[word]
    sims = [(w, cosine_sim(vec, v)) for w, v in word_vectors.items() if w != word]
    return sorted(sims, key=lambda x: x[1], reverse=True)[:top_n]

def word_analogy(a, b, c):
    """a is to b as c is to ?"""
    # result = vec(b) - vec(a) + vec(c)
    result = word_vectors[b] - word_vectors[a] + word_vectors[c]
    sims = [(w, cosine_sim(result, v)) for w, v in word_vectors.items()
            if w not in [a, b, c]]
    return sorted(sims, key=lambda x: x[1], reverse=True)[0]

print("🔍 Word Similarity Explorer")
print("="*50)

# Find similar words
for word in ["king", "dog", "computer"]:
    similar = find_similar(word, 3)
    print(f"\nWords similar to '{word}':")
    for w, score in similar:
        print(f"  {w:12} (similarity: {score:.2f})")

# Word analogies
print("\n" + "="*50)
print("🧮 Word Analogies:")
print("\nking:queen :: man:?")
answer, score = word_analogy("king", "queen", "man")
print(f"  Answer: {answer} (score: {score:.2f})")

print("\ndog:cat :: prince:?")
answer, score = word_analogy("dog", "cat", "prince")
print(f"  Answer: {answer} (score: {score:.2f})")
                        

💻 Hands-On: TF-IDF Vectorization

(Transform documents into weighted term vectors)


from sklearn.feature_extraction.text import TfidfVectorizer
import numpy as np

# Sample document corpus
documents = [
    "Machine learning is a subset of artificial intelligence",
    "Deep learning uses neural networks with many layers",
    "Natural language processing enables machines to understand text",
    "AI and machine learning are transforming industries"
]

# Create TF-IDF vectorizer
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(documents)

# Display vocabulary
print("📚 Vocabulary (sample):", list(vectorizer.vocabulary_.keys())[:8])
print(f"\n📊 TF-IDF Matrix Shape: {tfidf_matrix.shape}")
print("   (4 documents × {} unique terms)\n".format(len(vectorizer.vocabulary_)))

# Show TF-IDF scores for first document
feature_names = vectorizer.get_feature_names_out()
doc1_scores = tfidf_matrix[0].toarray()[0]
sorted_idx = np.argsort(doc1_scores)[::-1]

print("🔍 Top terms in Document 1:")
for i in sorted_idx[:5]:
    if doc1_scores[i] > 0:
        print(f"   '{feature_names[i]}': {doc1_scores[i]:.3f}")
                        

The Flaws of Static Embeddings

  • OOV (Out-of-Vocabulary): It utterly fails when encountering a word not present in its training corpus.
  • Morphological Blindness: Without subword tokenization, "run", "runner", and "running" are treated as totally separate entities.
  • Static Nature: Word2Vec assigns a single, permanent vector to the word "bat", permanently collapsing the distinction between the animal and the sports equipment.

Contextual Embeddings

The Dynamic Revolution (ELMo, BERT)

The ultimate solution to the Word2Vec "Static" limitation.

  • Mechanism: Before finalizing the vector representation of a word, the model dynamically reads the entire surrounding sentence.
  • The Result:
    • "He bought a new bat" → Generates the equipment vector.
    • "The bat flew away" → Generates the mammal vector.
  • This finally solved lexical ambiguity, pushing NLP into the modern era.

💻 Hands-On: Text Preprocessing Pipeline

(Tokenization, Stemming, and Lemmatization in action)


import simplemma
import re

# Sample text for processing
text = "The running foxes were quickly jumping over the fallen trees. They're loving it!"

# Step 1: Basic Tokenization
tokens = re.findall(r'\b\w+\b', text.lower())
print("🔤 Original Tokens:")
print(f"   {tokens}\n")

# Step 2: Lemmatization (reduce to base form)
lemmas = [simplemma.lemmatize(word, lang='en') for word in tokens]
print("📖 After Lemmatization:")
print(f"   {lemmas}\n")

# Step 3: Simple Stemming (rule-based suffix removal)
def simple_stem(word):
    suffixes = ['ing', 'ed', 'ly', 's', 'er', 'est']
    for suffix in suffixes:
        if word.endswith(suffix) and len(word) > len(suffix) + 2:
            return word[:-len(suffix)]
    return word

stems = [simple_stem(word) for word in tokens]
print("✂️ After Stemming:")
print(f"   {stems}\n")

# Compare transformations
print("📊 Comparison (Token → Lemma → Stem):")
for t, l, s in list(zip(tokens, lemmas, stems))[:6]:
    print(f"   {t:12} → {l:10} → {s}")
                        

Language Models (LMs)

From Reader to Creator

  • The Evolution: Machines that learned to read and classify text are now ready to generate it.
  • Core Philosophy: "What word comes next?" The entire premise is based on probabilistic prediction.
  • Just as human intuition completes sentences (e.g., "The sky is... -> blue"), AI trains on terabytes of textual data to map the statistical relationships between words and predict sequences.

First-Order Sequence Models

The "Goldfish Memory" Approach

Markov Chains: Predicting the next state based exclusively on the current state, ignoring all previous history.

P(next word | current word)

Example: "The cat sat on the [___]".
The model looks only at the word 'the' and checks its statistical history to see how often 'mat' or 'chair' followed it.

  • Severe Limitation: By looking only at one preceding word, it loses global context. (e.g., "The astronaut flew to the [tree]" - highly probable grammatically, but contextually absurd).

Transition Matrices

The AI's Probability Map

A massive lookup table storing the probability distribution of shifting from one specific word to another.

P(word₂ | word₁) = Count(word₁, word₂) / Count(word₁)

  • <start> → "Play" (100% chance)
  • "Play" → "the" (80% chance)
  • "the" → "music" (90% chance)
  • "music" → <end>

*Historical Note: Andrey Markov originally developed this by analyzing the consonant/vowel distribution in Alexander Pushkin's poetry.

💻 Code: Markov Generation

(Building a Transition Matrix using NLTK N-grams)


from nltk import ngrams, ConditionalFreqDist

text = "I love AI . I love Python . AI is amazing .".split()

# Extract Bigrams (Pairs of consecutive words)
bigrams = list(ngrams(text, 2))
print("Sample Bigrams:", bigrams[:4], "...\n")

# Build Conditional Frequency Distribution (Transition Matrix)
cfd = ConditionalFreqDist(bigrams)

print("Transition Matrix for 'AI':", dict(cfd['AI']))
print("Transition Matrix for 'love':", dict(cfd['love']))

# Next Word Prediction
current_word = "I"
# Max() returns the most statistically probable next word
next_word = cfd[current_word].max()
print(f"\nPrediction: After the word '{current_word}', the model predicts '{next_word}'.")
                        

💻 Hands-On: Text Generation with Markov

(Generate new text using learned transition probabilities)


import random
from collections import defaultdict

# Training corpus - Shakespeare-style text
corpus = """
To be or not to be that is the question.
To live or not to live is also a question.
To learn is to grow and to grow is to be wise.
Knowledge is power and power is wisdom.
Wisdom leads to understanding and understanding leads to peace.
""".lower()

words = corpus.split()

# Build transition probabilities
transitions = defaultdict(list)
for i in range(len(words) - 1):
    transitions[words[i]].append(words[i+1])

print("Transition options for 'to':", transitions['to'][:5])
print("Transition options for 'is':", transitions['is'][:5])
print("\n" + "="*40)

# Generate new text!
def generate_text(start_word, length=10):
    current = start_word
    result = [current]
    for _ in range(length - 1):
        if current in transitions:
            current = random.choice(transitions[current])
            result.append(current)
        else:
            break
    return ' '.join(result)

print("\n🎲 Generated Sentences:")
for i in range(3):
    print(f"  {i+1}. {generate_text('to', 8)}")
                        

Second-Order Markov Models

A Broader Horizon

To predict the next word, the model uses a sliding window to look at the two preceding words (Trigrams) instead of just one.

  • Advantage: The ambiguity of ("Play" -> ?) is resolved by analyzing ("Play", "the" -> "music"). Uncertainty drops drastically.
  • Skip-Grams: An advanced variation that skips over arbitrary filler words to establish relationships with distant, highly relevant keywords.

The Barrier of Statistical Models

Stochastic Parrots vs. True Intelligence

  • Human Perspective: We utilize world knowledge and common sense mental models to understand text.
  • Markov Models: They are heartless statistical engines. They don't know that "the sun rises in the west" is a physical impossibility; they only care if it's statistically probable in the training corpus.
  • Conclusion: Pure statistical frequency models are fundamentally incapable of deep semantic comprehension.

Sentiment Analysis

The Digital Empathy

The computational study of extracting subjective information, identifying whether the underlying emotional tone of a text is Positive, Negative, or Neutral.

  • Key Metrics:
    • Polarity: Ranges from -1 (Extremely Negative) to +1 (Extremely Positive).
    • Subjectivity: Ranges from 0 (Objective Fact) to 1 (Personal Opinion).
  • The LLM Revolution: Modern deep learning models can now detect highly nuanced emotional states, including sarcasm and irony, which traditionally defeated legacy NLP.

💻 Code: Sentiment Analysis

(Implementing VADER - Valence Aware Dictionary and sEntiment Reasoner)


from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer

# VADER is highly optimized for social media text and microblogs
analyzer = SentimentIntensityAnalyzer()

text = "I absolutely love this NLP course, it is incredibly engaging! But the homework is awful."

# Generate polarity scores
scores = analyzer.polarity_scores(text)
print("Input Text:", text)
print("\nRaw Scoring Metrics:", scores)

# The 'compound' score is a normalized, weighted composite score
compound = scores['compound']

if compound >= 0.05:
    print("\nOverall Sentiment: Positive 😊")
elif compound <= -0.05:
    print("\nOverall Sentiment: Negative 😠")
else:
    print("\nOverall Sentiment: Neutral 😐")
                        

💻 Hands-On: Build Your Own Sentiment Analyzer

(Lexicon-based approach from scratch)


import re

# Build a simple sentiment lexicon
positive_words = {
    'love': 3, 'amazing': 3, 'excellent': 3, 'good': 2,
    'great': 2, 'happy': 2, 'wonderful': 3, 'best': 3,
    'like': 1, 'nice': 1, 'enjoy': 2, 'fantastic': 3
}

negative_words = {
    'hate': -3, 'awful': -3, 'terrible': -3, 'bad': -2,
    'horrible': -3, 'worst': -3, 'boring': -2, 'poor': -2,
    'sad': -2, 'disappointing': -2, 'ugly': -2, 'difficult': -1
}

# Intensifiers and negations
intensifiers = {'very': 1.5, 'really': 1.5, 'extremely': 2, 'absolutely': 2}
negations = {'not', "n't", 'never', 'no'}

def analyze_sentiment(text):
    words = re.findall(r'\b\w+\b', text.lower())
    score = 0
    details = []
    negate = False
    intensity = 1

    for word in words:
        if word in negations:
            negate = True
            continue
        if word in intensifiers:
            intensity = intensifiers[word]
            continue

        word_score = positive_words.get(word, 0) + negative_words.get(word, 0)
        if word_score != 0:
            if negate:
                word_score = -word_score * 0.5
            word_score *= intensity
            details.append((word, word_score))
            score += word_score
            negate = False
            intensity = 1

    return score, details

# Test sentences
sentences = [
    "I love this amazing product!",
    "This is really terrible and awful",
    "The movie was not bad actually",
    "I'm very happy with this excellent service"
]

print("🎭 Custom Sentiment Analyzer\n" + "="*50)
for sent in sentences:
    score, details = analyze_sentiment(sent)
    emoji = "😊" if score > 0 else "😠" if score < 0 else "😐"
    print(f"\n'{sent}'")
    print(f"  Words found: {details}")
    print(f"  Total Score: {score:.1f} {emoji}")
                        

💻 Hands-On: Document Similarity Search

(Find similar documents using TF-IDF and cosine similarity)


from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np

# Document corpus
documents = [
    "Python is a great programming language for data science",
    "Machine learning algorithms can predict future outcomes",
    "Data science uses Python and machine learning techniques",
    "JavaScript is popular for web development",
    "Neural networks are a type of machine learning model",
    "Web applications often use JavaScript frameworks"
]

# Query to find similar documents
query = "What programming language is best for machine learning?"

# Vectorize documents and query
vectorizer = TfidfVectorizer(stop_words='english')
doc_vectors = vectorizer.fit_transform(documents)
query_vector = vectorizer.transform([query])

# Calculate cosine similarity
similarities = cosine_similarity(query_vector, doc_vectors)[0]

print("🔍 Semantic Document Search")
print("="*55)
print(f"Query: '{query}'\n")
print("Document Rankings:\n")

# Sort by similarity
ranked_idx = np.argsort(similarities)[::-1]

for rank, idx in enumerate(ranked_idx, 1):
    score = similarities[idx]
    bar = "█" * int(score * 25)
    print(f"  #{rank} (score: {score:.3f}) {bar}")
    print(f"     '{documents[idx][:50]}...'\n")

print("💡 TF-IDF + Cosine Similarity = Simple Semantic Search!")
                        

Neural Networks

Recurrent Neural Networks (RNN)

The Need for Sequence: How do we differentiate grammatical structures over time?

  • The RNN Solution: It reads tokens sequentially, maintaining a "Hidden State" (a digital notepad) that carries memory of previous words into the calculation of the next word.
  • The Villains (Drawbacks):
    • Vanishing Gradients (Amnesia): In long sentences, mathematical gradients diminish to zero, causing the network to "forget" the beginning of the sentence.
    • Sequential Bottleneck: Processing must happen one word at a time, preventing parallel GPU acceleration.

Long Short-Term Memory (LSTM)

Curing the Network's Amnesia

An advanced RNN architecture specifically engineered to carry long-term dependencies across vast sequences.

  • The Three Gates:
    1. Forget Gate: Decides which irrelevant historical data to aggressively purge.
    2. Input Gate: Determines what incoming new data is critical enough to store.
    3. Output Gate: Controls what information is exposed to the next step.
  • The Cell State: Information flows through a central highway via linear addition, mathematically preventing the Vanishing Gradient problem!

💻 Hands-On: RNN Forward Pass

(Simulate how an RNN processes sequential data)


import numpy as np

# Simple RNN forward pass simulation
np.random.seed(42)

# Initialize weights (small random values)
hidden_size = 4
input_size = 3
Wxh = np.random.randn(hidden_size, input_size) * 0.1  # Input to hidden
Whh = np.random.randn(hidden_size, hidden_size) * 0.1  # Hidden to hidden
bh = np.zeros(hidden_size)  # Hidden bias

def tanh(x):
    return np.tanh(x)

# Input sequence: "I love AI" (3 words as simple vectors)
sequence = [
    np.array([1, 0, 0]),  # "I"
    np.array([0, 1, 0]),  # "love"
    np.array([0, 0, 1])   # "AI"
]
words = ["I", "love", "AI"]

# Process sequence through RNN
h = np.zeros(hidden_size)  # Initial hidden state

print("🔄 RNN Processing Sequence:")
print("="*45)
for i, (x, word) in enumerate(zip(sequence, words)):
    # RNN formula: h_t = tanh(Wxh·x_t + Whh·h_{t-1} + b)
    h_new = tanh(np.dot(Wxh, x) + np.dot(Whh, h) + bh)
    print(f"\nStep {i+1}: Processing '{word}'")
    print(f"  Input vector: {x}")
    print(f"  Previous h:   [{', '.join(f'{v:.2f}' for v in h)}]")
    print(f"  New hidden:   [{', '.join(f'{v:.2f}' for v in h_new)}]")
    h = h_new

print("\n" + "="*45)
print("✅ Final hidden state encodes the entire sequence!")
                        

💻 Hands-On: LSTM Gates Simulation

(Visualize how LSTM gates control information flow)


import numpy as np

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def tanh(x):
    return np.tanh(x)

# Simulate LSTM gates for one timestep
print("🧠 LSTM Gate Simulation")
print("="*50)

# Simulated input (current word embedding + previous hidden state)
x_t = np.array([0.5, -0.2, 0.8])  # Current input
h_prev = np.array([0.1, 0.3])     # Previous hidden state
c_prev = np.array([0.4, -0.1])   # Previous cell state

# Simulated gate outputs (normally computed from weights)
forget_gate = sigmoid(np.array([0.2, 0.9]))  # What to forget
input_gate = sigmoid(np.array([0.8, 0.3]))   # What to write
candidate = tanh(np.array([0.5, -0.4]))      # New candidate values
output_gate = sigmoid(np.array([0.7, 0.6]))  # What to output

print(f"\n📥 Previous Cell State: {c_prev}")
print(f"\n🚪 Gate Activations:")
print(f"   Forget Gate: {np.round(forget_gate, 2)} (0=forget, 1=keep)")
print(f"   Input Gate:  {np.round(input_gate, 2)} (0=ignore, 1=write)")
print(f"   Output Gate: {np.round(output_gate, 2)} (0=hide, 1=expose)")

# LSTM cell state update: c_t = f*c_{t-1} + i*candidate
c_new = forget_gate * c_prev + input_gate * candidate
h_new = output_gate * tanh(c_new)

print(f"\n📤 New Cell State: {np.round(c_new, 3)}")
print(f"📤 New Hidden State: {np.round(h_new, 3)}")
print(f"\n💡 The cell state preserves long-term memory!")
print("   Forget gate kept {:.0%} of previous info".format(np.mean(forget_gate)))
                        

Seq2Seq Architecture

The Dual-Expert Alliance

How do we capture the "essence" of one language and generate an entirely new sequence? (e.g., Machine Translation, Summarization).

  • The Architecture:
    1. The Encoder: Reads the input sequence and compresses its semantic meaning into a dense mathematical representation.
    2. The Decoder: Receives this compressed representation and dynamically unwraps it to generate a new output sequence.

Deep Dive: The Encoder

  1. Tokenization: "Hello world" → [Hello, world]
  2. Vocabulary Indexing: Tokens mapped to integers.
  3. Embedding Layer: Integers expanded into dense continuous vectors.
  4. Recurrent Processing: An RNN/LSTM processes tokens step-by-step, updating its internal hidden state.
  5. The Context Vector: The final hidden state becomes the "Thought Vector"—a compressed mathematical summary of the entire input sentence.

Deep Dive: The Decoder

  1. Initial State: Inherits the Encoder's final Context Vector as its absolute starting point.
  2. Autoregressive Generation: Generates the first token, then feeds that output back into itself to predict the next token, creating a sequential chain reaction.
  3. Detokenization: Translates predicted vector probabilities back into human-readable text via the vocabulary matrix.
  4. Termination: Halts generation only when it predicts a special "EOS" (End of Sequence) token.

💻 Hands-On: Softmax & Next Word Prediction

(Convert raw scores to probability distribution)


import numpy as np

def softmax(x):
    """Convert raw logits to probability distribution"""
    exp_x = np.exp(x - np.max(x))  # Subtract max for numerical stability
    return exp_x / exp_x.sum()

# Simulated vocabulary
vocab = ["the", "cat", "sat", "on", "mat", "dog", "floor", ""]

# Raw logits from neural network (before softmax)
# Higher values = model thinks word is more likely
logits = np.array([1.2, 2.8, 0.5, 0.3, 2.1, 0.1, 0.8, -1.0])

print("📊 Language Model Next Word Prediction")
print("="*50)
print(f"Context: 'The _____'\n")

# Apply softmax to get probabilities
probs = softmax(logits)

# Display sorted by probability
sorted_idx = np.argsort(probs)[::-1]
print("Word Probabilities (sorted):")
for i in sorted_idx:
    bar = "█" * int(probs[i] * 30)
    print(f"  {vocab[i]:8} {probs[i]:.3f} {bar}")

# Sampling strategies
print("\n🎯 Decoding Strategies:")
print(f"  Greedy (argmax): '{vocab[np.argmax(probs)]}'")

# Temperature sampling
temp = 0.5
temp_probs = softmax(logits / temp)
print(f"  Low temp (0.5):  More confident, less creative")
print(f"  High temp (2.0): More random, more creative")
                        

💻 Hands-On: Seq2Seq Encoder

(Watch how input compresses into a context vector)


import numpy as np

# Simulate Seq2Seq Encoder for translation: "Hello world" → Context
print("🔄 Seq2Seq Encoder Simulation")
print("="*50)
print("Task: Encode 'Hello world' for translation\n")

# Step 1: Tokenization
tokens = ["Hello", "world", ""]
print(f"Step 1 - Tokens: {tokens}")

# Step 2: Embedding lookup (simplified)
embeddings = {
    "Hello": np.array([0.8, 0.2, 0.1, 0.5]),
    "world": np.array([0.3, 0.9, 0.4, 0.2]),
    "": np.array([0.0, 0.0, 0.0, 1.0])
}

print("\nStep 2 - Embeddings:")
for token in tokens:
    print(f"  '{token}': {embeddings[token]}")

# Step 3: RNN encoding (simplified)
hidden = np.zeros(4)
print("\nStep 3 - Sequential RNN Processing:")
for i, token in enumerate(tokens):
    # Simplified: hidden = tanh(embedding + 0.5*prev_hidden)
    hidden = np.tanh(embeddings[token] + 0.5 * hidden)
    print(f"  After '{token}': h = [{', '.join(f'{v:.2f}' for v in hidden)}]")

print("\n" + "="*50)
print("📦 Final Context Vector:")
print(f"   {np.round(hidden, 3)}")
print("\n💡 This single vector captures the meaning of")
print("   'Hello world' - ready for the Decoder!")
                        

The Seq2Seq Bottleneck

The Information Crush

  • The Flaw: Forcing an entire complex paragraph into a single, fixed-size Context Vector is like trying to summarize a 3-hour movie on a sticky note. Critical nuance is lost.
  • The Revolutionary Fix: Instead of relying on one static vector, what if the Decoder could look back at the entire original input sequence dynamically while generating every single new word?

Self-Attention Mechanism

The Magic Trick of Modern NLP

A mathematical mechanism allowing a model to calculate how strongly every word in a sequence relates to every other word simultaneously.

"The animal didn't cross the street because it was too tired."

How does the machine know what 'it' refers to? The street or the animal? Self-Attention assigns massive mathematical weight between 'it' and 'animal', instantly resolving the coreference.

💻 Code: Self-Attention Math

(Using Matrix Multiplication to calculate focus weightings)


import numpy as np

# Sequence: "Bank", "of", "River"
words = ["Bank", "of", "River"]

# Simplified Embeddings: [Water_Feature, Financial_Feature]
vectors = np.array([
    [0.9, 0.1],  # Bank (Assuming riverbank context here)
    [0.1, 0.1],  # of
    [0.8, 0.2]   # River
])

# Self-Attention Formula core: Q x K^T (Query matrix dot Key matrix)
attention_scores = np.dot(vectors, vectors.T)

print("Raw Attention Scores for 'Bank' against all words:")
print(np.round(attention_scores[0], 2))

# Identify which context word 'Bank' pays the most attention to
highest_attention_idx = np.argmax(attention_scores[0][1:]) + 1
print(f"\nThe word 'Bank' attends most strongly to: '{words[highest_attention_idx]}'")
print("💡 The model successfully contextualizes 'Bank' as a body of water!")
                        

💻 Hands-On: Scaled Dot-Product Attention

(Full Query-Key-Value Attention as used in Transformers)


import numpy as np

def softmax(x):
    exp_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
    return exp_x / exp_x.sum(axis=-1, keepdims=True)

print("🔍 Scaled Dot-Product Attention")
print("="*50)

# Sentence: "The cat sat"
words = ["The", "cat", "sat"]
d_k = 4  # Dimension of keys

# Word embeddings (simplified)
embeddings = np.array([
    [0.1, 0.2, 0.3, 0.4],  # The
    [0.5, 0.8, 0.2, 0.1],  # cat
    [0.3, 0.1, 0.7, 0.5]   # sat
])

# In real Transformers, Q, K, V are learned linear projections
# Here we use embeddings directly for simplicity
Q = K = V = embeddings

# Step 1: Compute attention scores (Q × K^T)
scores = np.dot(Q, K.T)
print("Step 1 - Raw Scores (Q × K^T):")
print(np.round(scores, 2))

# Step 2: Scale by sqrt(d_k) to prevent large values
scaled_scores = scores / np.sqrt(d_k)
print(f"\nStep 2 - Scaled by √{d_k}:")
print(np.round(scaled_scores, 2))

# Step 3: Apply softmax to get attention weights
attention_weights = softmax(scaled_scores)
print("\nStep 3 - Attention Weights (softmax):")
for i, word in enumerate(words):
    weights_str = ' '.join(f'{w:.2f}' for w in attention_weights[i])
    print(f"  '{word}' attends to: [{weights_str}]")

# Step 4: Compute output (weighted sum of Values)
output = np.dot(attention_weights, V)
print("\nStep 4 - Contextualized Output:")
print(f"  'cat' now encodes context from entire sequence!")
                        

💻 Hands-On: Positional Encoding

(How Transformers understand word order without recurrence)


import numpy as np

def positional_encoding(seq_len, d_model):
    """Generate sinusoidal positional encodings"""
    positions = np.arange(seq_len)[:, np.newaxis]
    dims = np.arange(d_model)[np.newaxis, :]

    # PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
    # PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
    angles = positions / np.power(10000, (2 * (dims // 2)) / d_model)

    pe = np.zeros((seq_len, d_model))
    pe[:, 0::2] = np.sin(angles[:, 0::2])  # Even indices
    pe[:, 1::2] = np.cos(angles[:, 1::2])  # Odd indices
    return pe

print("📍 Positional Encoding Visualization")
print("="*50)

# Generate positional encodings for 5 positions, 8 dimensions
seq_len, d_model = 5, 8
pe = positional_encoding(seq_len, d_model)

words = ["I", "love", "natural", "language", "processing"]

print("Each position gets a unique encoding:\n")
for i, word in enumerate(words):
    encoding = ' '.join(f'{v:+.2f}' for v in pe[i, :4])
    print(f"  Position {i} ('{word}'): [{encoding} ...]")

print("\n💡 Key Insight:")
print("  - Each position has a unique 'fingerprint'")
print("  - Similar positions have similar encodings")
print("  - Allows parallel processing (no RNN needed!)")

# Show that nearby positions are similar
dist_01 = np.linalg.norm(pe[0] - pe[1])
dist_04 = np.linalg.norm(pe[0] - pe[4])
print(f"\n  Distance(pos 0, pos 1): {dist_01:.3f}")
print(f"  Distance(pos 0, pos 4): {dist_04:.3f}")
                        

Transformers

The Heart of Generative AI

By discarding slow RNN architecture entirely and relying solely on Attention mechanisms ("Attention Is All You Need", 2017), the Transformer unlocked massive parallel processing capabilities.

  • The Future is Here: This exact architecture serves as the foundation for the world's most powerful Generative AI models, including GPT, BERT, and LLaMA.
  • Machines have finally transcended logic constraints, demonstrating the ability to reason, write poetry, code, and converse with human parity.

💻 Hands-On: Complete Text Classification Pipeline

(End-to-end ML pipeline: Preprocessing → Vectorization → Training → Prediction)


from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import numpy as np

# Training data: [text, category]
data = [
    ("Python tutorial for beginners", "tech"),
    ("Machine learning course online", "tech"),
    ("Best laptops for programming", "tech"),
    ("How to train neural networks", "tech"),
    ("Championship football match highlights", "sports"),
    ("World cup soccer finals", "sports"),
    ("Basketball playoffs schedule", "sports"),
    ("Tennis grand slam results", "sports"),
    ("Stock market analysis today", "finance"),
    ("Investment strategies for 2024", "finance"),
    ("Cryptocurrency trading tips", "finance"),
    ("Banking sector quarterly report", "finance"),
]

texts = [d[0] for d in data]
labels = [d[1] for d in data]

# Step 1: Vectorization (TF-IDF)
vectorizer = TfidfVectorizer(stop_words='english')
X = vectorizer.fit_transform(texts)

# Step 2: Train classifier (Naive Bayes)
classifier = MultinomialNB()
classifier.fit(X, labels)

print("🤖 Text Classification Pipeline")
print("="*50)
print(f"Training samples: {len(texts)}")
print(f"Categories: {list(set(labels))}")
print(f"Vocabulary size: {len(vectorizer.vocabulary_)}\n")

# Step 3: Predict new texts
test_texts = [
    "Deep learning frameworks comparison",
    "Premier league football scores",
    "How to invest in mutual funds",
    "New GPU for gaming and AI"
]

print("📊 Predictions on New Texts:\n")
for text in test_texts:
    vec = vectorizer.transform([text])
    pred = classifier.predict(vec)[0]
    probs = classifier.predict_proba(vec)[0]
    conf = max(probs) * 100
    print(f"  '{text}'")
    print(f"  → Category: {pred.upper()} (confidence: {conf:.0f}%)\n")
                        

🎯 The NLP Pipeline Summary

From Raw Data to Intelligent Generation

① Data Processing

  • Tokenization (Words/Subwords)
  • Stop Words Removal
  • Stemming & Lemmatization
  • POS Tagging & NER

② Feature Engineering

  • Bag of Words (BoW)
  • TF-IDF Vectorization
  • N-grams & Markov Models

③ Semantic Representation

  • Word Embeddings (Word2Vec)
  • Contextual Embeddings (BERT)
  • Cosine Similarity Metrics

④ Deep Learning Architectures

  • RNN & LSTM Networks
  • Seq2Seq Topologies
  • Self-Attention Mechanism
  • Transformers (LLMs)

💡 Key Takeaway: There is no ML without clean data. There is no comprehension without embeddings. Modern AI is the culmination of this entire pipeline.