Day 2 · NumPy: Arrays, Shapes & Matrix Math
Suggested pace: 1–1.5 hrs learn · 2–3 hrs code · rest to debug and document What you should know first ☐ Day 1: lists, loops, functions and import ☐ What a matrix shape such as (2, 3) means (rows, columns) ☐ Dot product
Suggested pace: 1–1.5 hrs learn · 2–3 hrs code · rest to debug and document
What you should know first ☐ Day 1: lists, loops, functions and import ☐ What a matrix shape such as (2, 3) means (rows, columns) ☐ Dot product from Day 1 (you will meet it again as @) If one of these feels shaky, spend 15 minutes revisiting it before starting. That is normal and saves time later.
Goal of today Learn NumPy, the tool that much of the Python machine-learning ecosystem is built on. Arrays let you do math on thousands of numbers at once, without writing loops.
You will be able to:
- Create arrays and read their shape
- Index, slice and filter arrays
- Do math on whole arrays (vectorization) and use broadcasting
- Reshape arrays and multiply matrices
- Compute cosine similarity without loops
0. Setup (do this first)
pip install numpy pandas matplotlib jupyter
jupyter notebook # or use VS Code notebooksIn every notebook, start with:
import numpy as np
import pandas as pd(np and pd are just short nicknames everyone uses.)
PART A — NUMPY
1. What is NumPy and why do we need it?
Problem: Python lists are slow for math. Suppose you want to multiply 1 million numbers by 2:
# Python list - slow (loops one by one in Python)
result = [x * 2 for x in my_list]
# NumPy - fast (one command, runs in optimized C code)
result = my_array * 2NumPy is often much faster than plain Python loops on large numeric arrays.
*Rule of ML: If you’re writing a `for` loop over numbers, there’s probably a NumPy way to do it without the loop. This is called vectorization*.
Worked Problem · Loop Versus Array
Problem. Double every number in [1, 2, 3], first with a Python loop and then with NumPy.
- With a loop you visit each number yourself and build a new list.
- With NumPy you write the math once and it is applied to every element:
a * 2. - Both give the same numbers, but NumPy is much faster on large data.
import numpy as np
nums = [1, 2, 3]
print([n * 2 for n in nums])
print(np.array(nums) * 2)[2, 4, 6]
[2 4 6]Answer. Both give 2, 4, 6. Use NumPy whenever you would otherwise loop over numbers.
2. Arrays — the core object
An array is a container of numbers arranged in a grid. All numbers must be the same type.
a = np.array([1, 2, 3])
print(a) # [1 2 3]2.1 Creating arrays
Think of it as a row of boxes: [ 1 ][ 2 ][ 3 ]
A 2D array (a table / matrix):
b = np.array([[1, 2, 3],
[4, 5, 6]]) col0 col1 col2
row 0 [ 1 2 3 ]
row 1 [ 4 5 6 ]2.2 Dimensions (ndim)
| Name | Example | ndim | Real-life meaning |
|---|---|---|---|
| Scalar | 5 | 0 | a single number |
| Vector (1D) | [1,2,3] | 1 | a list of numbers, e.g. one student’s 3 marks |
| Matrix (2D) | [[1,2],[3,4]] | 2 | a table, e.g. marks of many students |
| Tensor (3D+) | stack of matrices | 3+ | e.g. a color image (height × width × 3 colors) |
2.3 Shape — the most important concept
Shape tells you the size in each direction. It is written as a tuple.
b = np.array([[1, 2, 3],
[4, 5, 6]])
print(b.shape) # (2, 3)(2, 3) means 2 rows and 3 columns. Always (rows, columns).
For a 1D array [1,2,3] the shape is (3,) — a comma because it’s a tuple with one item.
*Key idea Many beginner ML/DL bugs come from shapes.* Get in the habit of writing print(x.shape) all the time.
Other useful attributes:
b.ndim # 2 number of dimensions
b.size # 6 total number of elements
b.dtype # int64 data typenp.zeros((2, 3)) # 2x3 filled with 0
np.ones((2, 3)) # 2x3 filled with 1
np.full((2, 2), 7) # 2x2 filled with 7
np.arange(0, 10, 2) # [0 2 4 6 8] (start, stop-not-included, step)
np.linspace(0, 1, 5) # [0. 0.25 0.5 0.75 1.] (5 evenly spaced numbers)
np.eye(3) # 3x3 identity matrix (1s on diagonal)
np.random.rand(2, 3) # random numbers between 0 and 1
np.random.randn(2, 3) # random numbers from normal distribution (mean 0)2.4 Handy ways to create arrays
Random seed — makes “random” numbers repeatable (important for reproducible results):
np.random.seed(42)Worked Problem · Describe An Array
Problem. A week of readings is stored as np.array([[22, 25, 19], [30, 28, 27]]). Find its shape, number of dimensions, total size and data type.
- Count the rows: 2. Count the columns: 3. So the shape is (2, 3).
- Each extra pair of brackets is one dimension. Here there are two levels, so
ndimis 2. - Size is the total number of values: 2 × 3 = 6.
- All values are whole numbers, so NumPy stores them as integers (usually
int64). Print each attribute to check.
import numpy as np
a = np.array([[22, 25, 19], [30, 28, 27]])
print(a.shape)
print(a.ndim)
print(a.size)
print(a.dtype)(2, 3)
2
6
int64Answer. Shape (2, 3), 2 dimensions, size 6, dtype int64 on most computers.
Your Turn · Build Arrays Quickly
Problem. Make the array [0, 2, 4, 6, 8] with arange, and an array of 5 evenly spaced numbers from 0 to 1 with linspace.
Try it on paper or in a notebook first. Then check the solution.
Solution — Step By Step
np.arange(start, stop, step)stops beforestop. To end at 8 with step 2, use stop = 10.np.linspace(start, stop, count)includes the stop value. Ask for 5 numbers between 0 and 1.
import numpy as np
print(np.arange(0, 10, 2))
print(np.linspace(0, 1, 5))[0 2 4 6 8]
[0. 0.25 0.5 0.75 1. ]Answer. [0 2 4 6 8] and [0. 0.25 0.5 0.75 1.]. Remember: arange excludes the stop, linspace includes it.
3. Indexing and Slicing (getting parts of an array)
Python counts from 0.
a = np.array([10, 20, 30, 40, 50])
a[0] # 10 first
a[ - 1] # 50 last
a[1:4] # [20 30 40] from index 1 up to (NOT including) 4
a[:3] # [10 20 30] first three
a[2:] # [30 40 50] from index 2 to end
a[::2] # [10 30 50] every 2nd element3.1 1D indexing
b = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
b[0, 1] # 2 row 0, column 1
b[1] # [4 5 6] whole row 1
b[:, 0] # [1 4 7] ALL rows, column 0 (the colon means "everything")
b[0:2, 1:] # [[2 3],[5 6]] rows 0-1, columns 1 onward3.2 2D indexing → array[row, column]
How to read b[:, 0] *: “colon = all rows, comma, column 0.”*
a = np.array([5, 12, 7, 20, 3])
a > 6 # [False True True True False]
a[a > 6] # [12 7 20] only values where True
a[(a > 6) & (a < 15)] # [12 7] use & for AND, | for OR, ~ for NOT3.3 Boolean masking (filtering) — VERY useful
Use parentheses around each condition!
Worked Problem · Slice And Filter
Problem. For a = np.array([10, 20, 30, 40, 50]) get (a) the middle three values, (b) the last two values, (c) the values bigger than 25. For the matrix m = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) get (d) the value in row 1, column 2 and (e) column 1.
- (a) Slice
a[1:4]. The start (1) is included, the stop (4) is not, so we get indexes 1, 2, 3. - (b) A negative start counts from the end:
a[-2:]. - (c)
a > 25makes a True/False array. Putting it inside brackets keeps only the True positions. - (d) For a matrix use
m[row, column]. Rows and columns count from 0, som[1, 2]. - (e)
m[:, 1]means all rows, column 1.
import numpy as np
a = np.array([10, 20, 30, 40, 50])
m = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(a[1:4])
print(a[-2:])
print(a[a > 25])
print(m[1, 2])
print(m[:, 1])[20 30 40]
[40 50]
[30 40 50]
6
[2 5 8]Answer. (a) [20 30 40] (b) [40 50] (c) [30 40 50] (d) 6 (e) [2 5 8].
Your Turn · Zero Out The Failures
Problem. Scores are [55, 82, 67, 91, 45, 78]. Make a copy where every score below 60 becomes 0, then count how many students passed.
Try it on paper or in a notebook first. Then check the solution.
Solution — Step By Step
- Make a copy first so the original is safe:
new = scores.copy(). - The mask
new < 60marks failures. Assign 0 through the mask:new[new < 60] =0. - Passing students are those whose score is still above 0:
(new > 0).sum(). A True counts as 1 when you sum.
import numpy as np
scores = np.array([55, 82, 67, 91, 45, 78])
new = scores.copy()
new[new < 60] = 0
print(new)
print((new > 0).sum())[ 0 82 67 91 0 78]
4Answer. [ 0 82 67 91 0 78] and 4 students passed.
4. Vectorization — math on whole arrays
a = np.array([1, 2, 3])
b = np.array([10, 20, 30])
a + b # [11 22 33] element by element
a * b # [10 40 90] element by element (NOT matrix multiplication!)
a * 2 # [2 4 6]
a ** 2 # [1 4 9]
np.sqrt(a) # [1. 1.41 1.73]
np.exp(a) # e^x for each
np.log(a) # natural log for each
a = np.array([[1, 2, 3],
[4, 5, 6]])
a.sum() # 21 everything
a.sum(axis = 0) # [5 7 9] sum DOWN the columns (collapses rows)
a.sum(axis = 1) # [6 15] sum ACROSS the rows (collapses columns)
a.mean(), a.max(), a.min(), a.std()
a.argmax() # index of the max valueAggregations
Understanding axis (everyone struggles with this) axis=0 means “go down the rows” → you get one result per column. axis=1 means “go across the columns” → you get one result per row.
axis=1 →
[[1, 2, 3], → sum = 6
axis=0 [4, 5, 6]] → sum = 15
↓ ↓ ↓ ↓
5 7 9Trick: the axis you name is the one that disappears. Shape (2,3) with axis=0 → shape (3,). With axis=1 → shape (2,).
keepdims=True keeps the dimension so shapes stay compatible (you’ll need this for cosine similarity):
a.sum(axis = 1, keepdims = True) # shape (2,1) instead of (2,)Worked Problem · Sums By Axis
Problem. Sales for 2 shops over 3 days are [[10, 20, 30], [5, 15, 25]] (rows = shops, columns = days). Find the total per day and the total per shop.
axis=0means “go down the rows”, so the rows collapse and you get one result per column: the total per day.axis=1means “go across the columns”, so you get one result per row: the total *per shop*.- By hand: day totals are 10+5=15, 20+15=35, 30+25=55. Shop totals are 10+20+30=60 and 5+15+25=45.
- Check with NumPy.
import numpy as np
sales = np.array([[10, 20, 30], [5, 15, 25]])
print(sales.sum(axis=0))
print(sales.sum(axis=1))[15 35 55]
[60 45]Answer. Per day [15 35 55], per shop [60 45]. The axis you name is the one that disappears.
Your Turn · Min-Max Scaling Without A Loop
Problem. Rescale [2, 4, 6, 8] to the range 0 to 1 using (x − min) ÷ (max − min), with no loop.
Try it on paper or in a notebook first. Then check the solution.
Solution — Step By Step
- Find the minimum (2) and maximum (8). The range is 8 − 2 = 6.
- Subtract the minimum from every value at once: [0, 2, 4, 6].
- Divide by the range: [0, 2/6, 4/6, 1] ≈ [0, 0.333, 0.667, 1].
- NumPy does steps 2 and 3 on the whole array in one expression.
import numpy as np
x = np.array([2, 4, 6, 8])
scaled = (x - x.min()) / (x.max() - x.min())
print(np.round(scaled, 3))[0. 0.333 0.667 1. ]Answer. [0. 0.333 0.667 1.]. The smallest value maps to 0 and the largest to 1.
5. Broadcasting — NumPy’s “auto-stretch”
Problem: what if you add arrays of different shapes?
A = np.array([[1, 2, 3],
[4, 5, 6]]) # shape (2,3)
v = np.array([10, 20, 30]) # shape (3,)
A + v
# [[11 22 33]
# [14 25 36]]NumPy “stretched” v to match every row of A:
[[1, 2, 3] [[10,20,30]
[4, 5, 6]] + [10,20,30]] ← v copied downThe two rules Compare shapes from the right side. For each pair of dimensions they must be: 1. equal, or 2. one of them is 1 (that one gets stretched)
| Shape A | Shape B | Works ? | Result |
|---|---|---|---|
| (2,3) | (3,) | ✓ | (2,3) |
| (2,3) | (2,1) | ✓ | (2,3) |
| (2,3) | (1,3) | ✓ | (2,3) |
| (2,3) | (2,) | ✗ | 3 vs 2 mismatch |
| (5,1) column: | (1,4) v.reshape(2,1) | ✓ or | (5,4) . v[:, None] |
Fix for the ✗ case: make it a
Real use in ML: normalize each column of data:
X_scaled = (X - X.mean(axis = 0)) / X.std(axis = 0)X is (rows, features), mean(axis=0) is (features,) → broadcasts across all rows. No loops!
Worked Problem · Center Each Column
Problem. Subtract each column's mean from X = [[1, 2], [3, 4], [5, 9]] so every column has mean 0.
- The shape of X is (3, 2). Column means need
axis=0: column 0 is (1+3+5)÷3 = 3, column 1 is (2+4+9)÷3 = 5. So the means are [3, 5], shape (2,). - Subtract (3, 2) minus (2,). Broadcasting lines the shapes up from the right: the length-2 vector is stretched down all 3 rows.
- Row 1: [1−3, 2−5] = [−2, −3]. Row 2: [0, −1]. Row 3: [2, 4].
- Check by printing the new column means: they should be 0.
import numpy as np
X = np.array([[1, 2], [3, 4], [5, 9]])
means = X.mean(axis=0)
centered = X - means
print(means)
print(centered)
print(centered.mean(axis=0))[3. 5.]
[[-2. -3.]
[ 0. -1.]
[ 2. 4.]]
[0. 0.]Answer. Means [3. 5.], centered data [[-2. -3.] [ 0. -1.] [ 2. 4.]], new means *[0. 0.]*.
Your Turn · Will These Shapes Broadcast?
Problem. Decide for each pair whether it works, and give the result shape: (a) (3, 1) + (1, 4), (b) (2, 3) + (3,), (c) (2, 3) + (2,).
Try it on paper or in a notebook first. Then check the solution.
Solution — Step By Step
- Compare the shapes from the right. Each pair of numbers must be equal, or one of them must be 1.
- (a) 1 vs 4 → ok (4). 3 vs 1 → ok (3). Result (3, 4).
- (b) 3 vs 3 → ok. The missing left side is treated as 1. Result (2, 3).
- (c) 3 vs 2 → neither is 1, so it fails.
- Test all three with code.
import numpy as np
print((np.ones((3, 1)) + np.ones((1, 4))).shape)
print((np.ones((2, 3)) + np.ones(3)).shape)
try:
np.ones((2, 3)) + np.ones(2)
except ValueError as e:
print("ValueError:", e)(3, 4)
(2, 3)
ValueError: operands could not be broadcast together with shapes (2,3) (2,)Answer. (a) works, (3, 4); (b) works, (2, 3); (c) fails with a ValueError. To fix (c), use np.ones((2, 1)).
6. Reshape
Change the shape without changing the data (total size must stay the same).
x = np.arange(12) # [0 1 2 ... 11], shape (12,)
x.reshape(3, 4) # 3 rows, 4 columns
x.reshape(2, 6)
x.reshape( - 1, 4) # -1 means "you figure it out" → (3,4)
x.reshape(3, 4).flatten() # back to 1D-1 is a joker: NumPy calculates it. 12 / 4 = 3 rows.
Transpose (swap rows and columns):
A = np.array([[1,2,3],[4,5,6]]) # (2,3)
A.T # (3,2)Add a dimension:
v = np.array([1,2,3]) # (3,)
v[:, None] # (3,1) column vector
v[None, :] # (1,3) row vectorWorked Problem · Reshape A Flat Array
Problem. Turn np.arange(12) into a 3 × 4 matrix, then into a matrix with 2 rows where NumPy works out the columns.
np.arange(12)has 12 values. A reshape keeps all 12, so rows × columns must equal 12.- 3 × 4 = 12, so
reshape(3, 4)works. - For 2 rows, use
-1for the column count. NumPy computes 12 ÷ 2 = 6. - Print the shapes to confirm.
import numpy as np
a = np.arange(12)
print(a.reshape(3, 4))
print(a.reshape(2, -1).shape)[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
(2, 6)Answer. A 3×4 matrix with rows [0 1 2 3], [4 5 6 7], [8 9 10 11], and the second shape is (2, 6).
Your Turn · Flatten Images
Problem. A batch holds 10 images of 28 × 28 pixels, shape (10, 28, 28). Reshape it so each image becomes one row of 784 numbers.
Try it on paper or in a notebook first. Then check the solution.
Solution — Step By Step
- One image has 28 × 28 = 784 pixels.
- We want 10 rows (one per image) and 784 columns.
- Use
reshape(10, -1)and let NumPy compute the 784, orreshape(10, 784).
import numpy as np
batch = np.zeros((10, 28, 28))
flat = batch.reshape(10, -1)
print(flat.shape)(10, 784)Answer. The new shape is (10, 784). This is exactly how images are fed to a simple neural network.
7. Matrix Multiplication (the heart of neural networks)
Two different things:
A * B→ element-wise (multiply matching positions)A @ B→ matrix multiplication
How matrix multiplication works Each output cell = *dot product of a row of A with a column of B*.
A = [[1, 2], B = [[5, 6],
[3, 4]] [7, 8]]A @ B:
[0,0] = 1*5 + 2*7 = 19
[0,1] = 1*6 + 2*8 = 22
[1,0] = 3*5 + 4*7 = 43
[1,1] = 3*6 + 4*8 = 50Result = [[19, 22],
[43, 50]]The shape rule (m, n) @ (n, p) → (m, p) The inner numbers must match (n and n) and they disappear. Outer numbers become the result shape.
A = np.random.randn(2, 3)
B = np.random.randn(3, 4)
(A @ B).shape # (2, 4)If you get an error like “shapes not aligned” → inner dimensions don’t match → use .T or reshape.
Why does this matter? A neural network layer is literally:
output = X @ W + bX= (number of samples, number of input features)W= (input features, number of neurons)- Result = (samples, neurons)
So matrix multiplication computes the layer for all samples at once.
a = np.array([1,2,3]) ; b = np.array([4,5,6])
np.dot(a, b) # 1*4 + 2*5 + 3*6 = 32Dot product of vectors
Worked Problem · One Neural-Network Layer
Problem. Two samples with 3 features each are X = [[1, 2, 3], [4, 5, 6]]. A layer has weights W = [[1, 0], [0, 1], [1, 1]] and bias b = [10, 20]. Compute X @ W + b.
- Check shapes: X is (2, 3) and W is (3, 2). The inner numbers match (3), so the result is (2, 2).
- Row 1 of X times column 1 of W: 1×1 + 2×0 + 3×1 = 4. Times column 2: 1×0 + 2×1 + 3×1 = 5.
- Row 2 of X: 4×1 + 5×0 + 6×1 = 10, and 4×0 + 5×1 + 6×1 = 11. So
X @ W= [[4, 5], [10, 11]]. - Add the bias [10, 20] to every row (broadcasting): [[14, 25], [20, 31]].
import numpy as np
X = np.array([[1, 2, 3], [4, 5, 6]])
W = np.array([[1, 0], [0, 1], [1, 1]])
b = np.array([10, 20])
print(X @ W)
print(X @ W + b)[[ 4 5]
[10 11]]
[[14 25]
[20 31]]Answer. [[14, 25], [20, 31]]. This is the entire forward pass of a layer: multiply, then add the bias.
Your Turn · Shape Of A Hidden Layer
Problem. A batch of 32 images, each flattened to 784 numbers, goes into a layer with 128 neurons. What shape must the weight matrix have, and what is the output shape?
Try it on paper or in a notebook first. Then check the solution.
Solution — Step By Step
- The input is (32, 784).
- For
X @ Wthe inner numbers must match, so W needs 784 rows. It needs 128 columns, one per neuron: (784, 128). - Output shape = outer numbers = (32, 128).
- Verify with zeros.
import numpy as np
X = np.zeros((32, 784))
W = np.zeros((784, 128))
print((X @ W).shape)(32, 128)Answer. W is (784, 128) and the output is (32, 128): 128 numbers for each of the 32 images.
8. Basic Linear Algebra
np.linalg.norm(a) # length of vector = sqrt(a1² + a2² + ...)
np.linalg.inv(A) # inverse of square matrix
np.linalg.det(A) # determinant
np.linalg.norm(A, axis = 1) # length of EACH rowNorm example: [3,4] → sqrt(9+16) = 5. It’s the “length” of the arrow.
Worked Problem · Length And Angle
Problem. Find the length (norm) of [3, 4], scale it to a unit vector, and compute the cosine similarity of [1, 0, 1] and [0, 1, 1].
- Norm = √(3² + 4²) = √25 = 5.
- Unit vector = vector ÷ norm = [3/5, 4/5] = [0.6, 0.8].
- Cosine similarity = (a·b) ÷ (‖a‖ ‖b‖). The dot product is 1×0 + 0×1 + 1×1 = 1.
- Each vector has norm √2, so the bottom is √2 × √2 = 2. Cosine = 1 ÷ 2 = 0.5.
import numpy as np
v = np.array([3, 4])
print(np.linalg.norm(v))
print(v / np.linalg.norm(v))
a = np.array([1, 0, 1]); b = np.array([0, 1, 1])
print(round(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)), 3))5.0
[0.6 0.8]
0.5Answer. Norm 5.0, unit vector [0.6 0.8], cosine similarity 0.5 (about 60° apart).
Your Turn · Same Direction Or Not?
Problem. What is the cosine similarity of [2, 0] and [0, 3]? And of [1, 2] and [2, 4]?
Try it on paper or in a notebook first. Then check the solution.
Solution — Step By Step
[2, 0]and[0, 3]: the dot product is 2×0 + 0×3 = 0. A zero numerator means cosine = 0 (they point at right angles).[1, 2]and[2, 4]: the second is just double the first, so they point the same way. Dot = 1×2 + 2×4 = 10. Norms: √5 and √20. √5 × √20 = √100 = 10. Cosine = 10 ÷ 10 = 1.- Confirm with code.
import numpy as np
def cos(a, b):
a, b = np.array(a), np.array(b)
return a @ b / (np.linalg.norm(a) * np.linalg.norm(b))
print(cos([2, 0], [0, 3]))
print(round(cos([1, 2], [2, 4]), 6))0.0
1.0Answer. 0.0 (unrelated) and 1.0 (same direction). Cosine ignores length and only compares direction.
9. ★ ASSIGNMENT 1 — Cosine Similarity Without Loops
9.1 What is it?
Imagine every sentence is converted into a list of numbers (an embedding). To know if two sentences have similar meaning, we compare their number-lists. *Cosine similarity measures the angle* between two arrows:
| Value | Meaning |
|---|---|
| 1 | Same direction → very similar |
| 0 | Right angle → unrelated |
| -1 | Opposite direction |
9.2 Formula
cos(a, b) = (a · b) / (‖a‖ × ‖b‖)
Top = dot product. Bottom = lengths multiplied.
Worked Problem · Cosine Similarity By Hand
Problem. Find the cosine similarity of a = [3, 4] and b = [4, 3].
- Dot product: 3×4 + 4×3 = 24.
- Lengths: ‖a‖ = √(9+16) = 5 and ‖b‖ = 5.
- Cosine = 24 ÷ (5 × 5) = 0.96.
- Check with NumPy.
import numpy as np
a = np.array([3, 4]); b = np.array([4, 3])
print(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))0.96Answer. 0.96: the two arrows point almost the same way.
9.3 Worked example
a = [1, 0], b = [1, 1]
- a·b = 1×1 + 0×1 = 1 - ‖a‖ = 1, ‖b‖ = √2 ≈ 1.414
- cos = 1 / 1.414 = 0.707 (45° angle)
def cosine(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))9.4 Two vectors (easy)
9.5 Two MATRICES (the assignment)
A has shape (n, d): n embeddings, each with d numbers. B has shape (m, d): m embeddings. Goal: an (n, m) table where cell [i, j] = similarity between A’s row i and B’s row j.
The trick: if every vector has length 1, cosine = just the dot product. So: 1. Normalize every row to length 1 2. One matrix multiplication gives ALL pairs
def cosine_sim_matrix(A, B):
# Step 1: length of each row. keepdims=True keeps shape (n,1) so division
# broadcasts
A_norm = A / np.linalg.norm(A, axis = 1, keepdims = True) # (n,d)
B_norm = B / np.linalg.norm(B, axis = 1, keepdims = True) # (m,d)
# Step 2: (n,d) @ (d,m) -> (n,m)
return A_norm @ B_norm.TTest it:
A = np.random.randn(4, 8)
B = np.random.randn(5, 8)
S = cosine_sim_matrix(A, B)
print(S.shape) # (4, 5)
print(S.min(), S.max()) # always between -1 and 1
# Sanity check: similarity of A with itself has 1s on the diagonal
print(np.round(np.diag(cosine_sim_matrix(A, A)), 3)) # [1. 1. 1. 1.]Verify against loop version (to prove it is correct):
S_loop = np.zeros((4,5))
for i in range(4):
for j in range(5):
S_loop[i,j] = cosine(A[i], B[j])
print(np.allclose(S, S_loop)) # TrueMove on when you can… ☐ create an array and read its shape ☐ index, slice and filter with a boolean mask ☐ predict which axis disappears in sum(axis=0) and sum(axis=1)
☐ say whether two shapes broadcast and what the result shape is ☐ compute a cosine similarity without a loop If you cannot tick most of these, repeat the worked problems for this day. Taking an extra day is fine; it is not falling behind.