Vectors, Matrices and Why Models Are Multiplications
Dot products, matrix shapes, distance, and the one line most models are
You do not need a maths degree to do machine learning well. You do need four ideas, and this week covers them by implementing each one rather than proving anything. Today: why almost every model is, underneath, a matrix multiplication.
A row of data is a vector
One customer, tenure 12, charge 79.50, support calls 2, is a point in three-dimensional space. Two thousand customers are two thousand points. Everything that follows is geometry on those points.
customer = np.array([12.0, 79.50, 2.0])
weights = np.array([-0.05, 0.02, 0.30])
score = np.dot(customer, weights)
manual = sum(c * w for c, w in zip(customer, weights))
print('dot product : %.4f' % score)
print('by hand : %.4f' % manual)
by hand : 1.5900
The whole dataset at once
You never score one customer. You score all of them, and the matrix product does every dot product in one call.
X = np.array([[12.0, 79.50, 2.0],
[48.0, 55.00, 0.0],
[ 3.0, 92.00, 5.0],
[60.0, 21.00, 1.0]])
w = np.array([-0.05, 0.02, 0.30])
b = -1.2
scores = X @ w + b
print('X shape', X.shape, '@ w shape', w.shape, '-> ', scores.shape)
print('scores', scores.round(3))
scores [ 0.39 -2.5 1.99 -3.48]
That line is a linear model
X @ w + b is the entire forward pass of linear regression, and with a sigmoid wrapped round it, of logistic regression. Week 11's neural network is the same line repeated with a nonlinearity between the repeats. Understand this one expression and you understand the shape of most of the course.
Shapes have to line up
A matrix product (n, k) @ (k, m) gives (n, m). The inner dimensions must match, and this is the source of most errors you will hit in week 11.
X = np.random.default_rng(0).normal(size=(100, 4)) # 100 rows, 4 features
W1 = np.random.default_rng(1).normal(size=(4, 8)) # layer: 4 in, 8 out
W2 = np.random.default_rng(2).normal(size=(8, 1)) # layer: 8 in, 1 out
h = X @ W1
out = h @ W2
print('X ', X.shape)
print('X@W1', h.shape)
print('h@W2', out.shape)
try:
X @ W2
except ValueError as e:
print('\nmismatch:', e)
X@W1 (100, 8)
h@W2 (100, 1)
mismatch: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 8 is different from 4)
Distance, and what it means for a model
kNN, k-means and every clustering algorithm in week 9 rest on measuring how far apart two rows are.
a = np.array([12.0, 79.5])
b = np.array([14.0, 81.0])
c = np.array([60.0, 21.0])
def euclid(p, q):
return np.sqrt(((p - q) ** 2).sum())
print('a to b: %.2f' % euclid(a, b))
print('a to c: %.2f' % euclid(a, c))
print('numpy : %.2f' % np.linalg.norm(a - c))
a to c: 75.67
numpy : 75.67
Distance is meaningless on unscaled columns
Tenure runs 1 to 72; total charges run to 8000. In the distance calculation, charges dominate entirely and tenure might as well not exist. Every distance-based algorithm requires scaling first, and this is the real reason StandardScaler appears in nearly every pipeline in this course.
from sklearn.preprocessing import StandardScaler
raw = np.array([[12.0, 950.0],
[14.0, 8000.0],
[60.0, 1000.0]])
print('unscaled: row0-row1 %.1f, row0-row2 %.1f'
% (np.linalg.norm(raw[0] - raw[1]), np.linalg.norm(raw[0] - raw[2])))
scaled = StandardScaler().fit_transform(raw)
print('scaled : row0-row1 %.2f, row0-row2 %.2f'
% (np.linalg.norm(scaled[0] - scaled[1]),
np.linalg.norm(scaled[0] - scaled[2])))
scaled : row0-row1 2.13, row0-row2 2.17
Unscaled, row 0 looks far closer to row 2 than to row 1, because the charge difference swamps everything. Scaled, the answer flips, and the scaled answer is the one that respects both columns.
Day 1 takeaway
A row is a vector, a dataset is a matrix, and a linear model's prediction isX @ w + b. Matrix shapes must agree on the inner dimension. Distance underpins every clustering and neighbour method, and is meaningless until the columns share a scale.