1. Vectors & Matrices
You don't need a semester of linear algebra to start — you need to recognize three objects, because they're the literal building blocks of every model from Week 11 onward.
- Scalar — a single number (e.g. a learning rate)
- Vector — an ordered list of numbers (e.g. one data point's features, or one word's embedding)
- Matrix — a 2D grid of numbers (e.g. a whole dataset, or a neural network layer's weights)
import numpy as np
v = np.array([1.5, 0.0, 3.2]) # a vector, shape (3,)
M = np.array([[1, 2, 3], [4, 5, 6]]) # a matrix, shape (2, 3) -- 2 rows, 3 columns
# The transpose flips rows and columns
M_T = M.T # shape (3, 2)
The transpose shows up constantly in the notation you'll read later —
a weight matrix applied one way in a forward pass is frequently transposed to compute
a gradient going backward, and you'll see Wᵀ throughout the transformer
paper in Week 16.
2. The Dot Product & Matrix Multiplication
The one operation to internalize now is the dot product — multiply corresponding elements of two vectors, then sum the results:
features = np.array([1.5, 0.0, 3.2]) # one data point
weights = np.array([0.4, 0.9, -0.1]) # a model's learned weights
# Dot product, by hand
manual = sum(f * w for f, w in zip(features, weights))
# Dot product, the NumPy way
dot = features @ weights # or: np.dot(features, weights)
print(manual, dot) # 0.28 0.28 -- same result
That single line, features @ weights, is — quite literally — what a neuron
in a neural network computes before its activation function is applied.
Matrix multiplication is just this same dot product done for every
row/column pair at once:
A = np.array([[1, 2], [3, 4]]) # shape (2, 2)
B = np.array([[5, 6], [7, 8]]) # shape (2, 2)
C = A @ B
# C[i, j] = dot product of A's row i and B's column j
# C = [[1*5+2*7, 1*6+2*8],
# [3*5+4*7, 3*6+4*8]]
# = [[19, 22],
# [43, 50]]
# The one rule that matters: inner dimensions must match.
# A (2, 3) @ B (3, 4) is valid -> result is (2, 4).
# A (2, 3) @ B (2, 4) raises a ValueError -- the 3 and 2 don't line up.
A batch of 32 data points, each with 3 features, run through a layer with 3 inputs and
4 outputs is exactly one matrix multiplication: (32, 3) @ (3, 4) → (32, 4).
Every forward pass in Week 11's neural network, and every attention score in Week 15's
transformer, is this same shape-matching rule applied over and over.
"Inner dimensions must match" is the rule you'll debug against most often once you're building networks. If a matrix multiplication fails, the fix is almost always to transpose one side or double-check which axis is "features" vs. "batch."
3. Vector Norms & Cosine Similarity
A norm measures a vector's length. The two you'll see everywhere: the L1 norm (sum of absolute values) and the L2 norm (square root of the sum of squares — ordinary Euclidean length).
v = np.array([3.0, -4.0])
l1 = np.sum(np.abs(v)) # 7.0
l2 = np.sqrt(np.sum(v ** 2)) # 5.0 -- or: np.linalg.norm(v)
Norms alone measure size, not direction. To compare how similar two vectors
are — regardless of their length — you use cosine similarity: the dot
product of two vectors, divided by the product of their norms. It ranges from
-1 (opposite direction) to 1 (same direction), with
0 meaning perpendicular/unrelated.
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
doc_a = np.array([1.0, 2.0, 0.0])
doc_b = np.array([2.0, 4.0, 0.0]) # same direction as doc_a, just longer
doc_c = np.array([0.0, 0.0, 5.0]) # perpendicular to doc_a
print(cosine_similarity(doc_a, doc_b)) # 1.0 -- identical direction
print(cosine_similarity(doc_a, doc_c)) # 0.0 -- unrelated
Cosine similarity is exactly how a vector database ranks "which documents are most relevant to this query" in Week 22's RAG pipeline — every embedding-based search you'll build comes down to this one formula.
4. Probability Foundations
You'll go deeper on probability as it comes up (evaluation metrics in Week 6, sampling in Week 17), but a few ideas are worth having solid now, because they show up immediately once you start describing data or a model's confidence.
- Mean — the average value; the "center" of your data
- Standard deviation — how spread out the values typically are from that center
- Random variable — a quantity whose value comes from some uncertain process (a die roll, tomorrow's stock price, a model's next-token prediction)
- Probability distribution — an assignment of likelihood to every possible value a random variable can take, always summing (or integrating) to
1
scores = np.array([72, 85, 90, 61, 78, 95, 88])
mean = scores.mean() # 81.28...
std = scores.std() # 10.9... -- typical distance from the mean
normalized = (scores - mean) / std # z-scores: how many std-devs from the mean
A small standard deviation means most values cluster tightly around the mean; a large one means they're spread wide. You'll use exactly this pair — mean and standard deviation — to normalize features before training a model in Week 5, and to interpret how "confident" a distribution over outcomes is: a tight distribution means a model is sure; a wide one means it's genuinely uncertain.
5. Conditional Probability & Bayes' Theorem
Conditional probability, written P(A | B), is "the
probability of A, given that we already know B happened." Bayes' theorem
is the formula for flipping a conditional probability around — going from
P(evidence | hypothesis), which is usually easy to estimate, to
P(hypothesis | evidence), which is usually what you actually want to know:
P(H | E) = P(E | H) * P(H) / P(E)
# H = hypothesis (e.g. "this email is spam")
# E = evidence (e.g. "this email contains the word 'free'")
Worked example — a classic spam filter setup:
# Priors and likelihoods, estimated from a labeled dataset
p_spam = 0.30 # P(spam) -- 30% of all mail is spam
p_ham = 0.70 # P(not spam)
p_free_given_spam = 0.60 # P("free" appears | spam)
p_free_given_ham = 0.05 # P("free" appears | not spam)
# P(E) -- total probability the word "free" appears at all, spam or not
p_free = (p_free_given_spam * p_spam) + (p_free_given_ham * p_ham)
# Bayes' theorem: flip P(E | H) into P(H | E)
p_spam_given_free = (p_free_given_spam * p_spam) / p_free
print(round(p_spam_given_free, 3)) # 0.837 -- 83.7% spam, given it contains "free"
Notice the result (84%) is much higher than the raw prior (30%) — seeing the word "free" is strong evidence, and Bayes' theorem is precisely the mechanism for updating a belief once new evidence arrives. This exact update rule — prior belief, new evidence, updated belief — is the conceptual backbone behind how you'll reason about model evaluation and calibration in Week 6.
6. From Raw Scores to a Probability Distribution: Softmax
A model rarely outputs a probability directly — it outputs raw, unbounded numbers
called logits (one per possible outcome). Softmax is
the function that turns any list of logits into a valid probability distribution:
every value between 0 and 1, and the whole set summing to
exactly 1.
def softmax(logits):
shifted = logits - np.max(logits) # subtract max first -- numerical stability
exp_scores = np.exp(shifted)
return exp_scores / np.sum(exp_scores)
logits = np.array([2.0, 1.0, 0.1])
probs = softmax(logits)
print(probs) # [0.659, 0.242, 0.099]
print(probs.sum()) # 1.0 -- always, by construction
Two details worth noticing: exponentiating first means larger logits become
disproportionately more likely — softmax exaggerates differences rather than
preserving them linearly. And subtracting np.max(logits) before
exponentiating doesn't change the final result (it cancels out in the division) but
keeps the numbers from overflowing — a real numerical-stability trick you'll see in
nearly every from-scratch transformer implementation in Week 16.
Softmax converts attention scores (computed via the matrix multiplication from Section 2) into attention weights in Week 15, and converts an LLM's final layer output into a probability distribution over its entire vocabulary — the actual mechanism behind picking the "next token" in Week 17.
7. Hands-on Exercise
Build a tiny document search engine with cosine similarity
You're given five short "documents," already converted into simple word-count vectors over a shared vocabulary — no NLP library needed yet, just the math from this week.
import numpy as np
# Vocabulary, in order: [cat, dog, python, code, bark, meow]
docs = {
"d1": np.array([2, 0, 1, 1, 0, 1]), # about a cat that codes in python
"d2": np.array([0, 3, 0, 0, 2, 0]), # about a dog that barks
"d3": np.array([1, 1, 2, 3, 0, 0]), # about python code, briefly mentions both animals
"d4": np.array([0, 0, 3, 3, 0, 0]), # purely about python code
"d5": np.array([3, 0, 0, 0, 0, 2]), # purely about a cat meowing
}
query = np.array([0, 0, 2, 2, 0, 0]) # a search for "python code"
Requirements:
- Implement
cosine_similarity(a, b)yourself (don't import it from a library) using@andnp.linalg.norm. - Compute the cosine similarity between
queryand every document indocs, and print the documents ranked from most to least similar. - Confirm
d4(purely about code) ranks aboved1andd3(mixed topics) — explain in one sentence why, in terms of the vectors' direction. - Write a
softmaxfunction from scratch and apply it to the five similarity scores from step 2, turning them into a probability distribution over "which document is most relevant." - Using made-up but reasonable numbers, apply Bayes' theorem to answer: "given that a document mentions 'python', what's the probability it's about coding rather than animals?" State your priors and likelihoods explicitly before computing.
Stack the five document vectors into one (5, 6) matrix and compute all five dot products with query in a single matrix-vector multiplication, instead of looping — good practice for the vectorized thinking from Week 1.
8. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why must the "inner dimensions" of two matrices match for A @ B to work?
Why must the "inner dimensions" of two matrices match for A @ B to work?
Each output entry C[i, j] is a dot product between a row of A and a column of B — a dot product requires both vectors to have the same length. A's row length (its number of columns) must therefore equal B's column length (its number of rows), which is exactly the "inner dimensions" rule: (m, n) @ (n, p) → (m, p).
Q2
Two vectors point in the exact same direction, but one is twice as long. What is their cosine similarity, and why?
Two vectors point in the exact same direction, but one is twice as long. What is their cosine similarity, and why?
1.0. Cosine similarity divides the dot product by the product of both norms, which exactly cancels out any difference in length — it measures direction only, not magnitude. This is why it's preferred over raw dot product for comparing embeddings of different "lengths" (e.g. a short query vs. a long document).
Q3
In Bayes' theorem, what's the practical difference between the "prior" and the "posterior"?
In Bayes' theorem, what's the practical difference between the "prior" and the "posterior"?
The prior, P(H), is what you believed before seeing any evidence (e.g. "30% of mail is spam"). The posterior, P(H | E), is your updated belief after incorporating a specific piece of evidence (e.g. "84% spam, now that we've seen the word 'free'"). Bayes' theorem is the exact arithmetic for that update.
Q4
Why subtract np.max(logits) before exponentiating in a softmax implementation?
Why subtract np.max(logits) before exponentiating in a softmax implementation?
It's purely for numerical stability — large logits can make np.exp() overflow to inf. Subtracting the maximum shifts every value to be ≤ 0 before exponentiating, so the largest term becomes exp(0) = 1 and everything else is a safe fraction. Because softmax divides by the sum of all terms, this shift cancels out exactly and never changes the final probabilities.