Why It's Not Element-Wise
Adding matrices is element-wise — easy. Multiplying matrices is not element-wise. It's a careful dance of rows and columns: each cell of the output is the dot product of one row of the left matrix and one column of the right matrix.
The shape rule: . Inner dimensions must match; outer dimensions form the result. Get this rule wrong and your code crashes; get it right and you've built a neural network layer.
Composing Transformations
Matrix multiplication encodes composing transformations. Rotate, then scale, then translate — each is a matrix; multiplying them gives a single matrix that does all three at once. Order matters: is generally not equal to . Rotate-then-scale ≠ scale-then-rotate (in 3D, vividly so).
Broadcasting Sneaks Back In
NumPy/PyTorch let you "multiply" a matrix by a vector with * (element-wise) — and broadcasting will silently apply the vector across rows or columns of the matrix. This is not matrix multiplication; it's element-wise op with broadcasting. Use @ for true matrix multiplication.
output = weights * inputs in a forward pass. The shapes happened to broadcast, the loss came out as a number, training even improved a bit. It took an hour to realize my "linear layer" was element-wise multiplication and not a real linear transformation. Use @. Every time.
import numpy as np
R = np.array([[0., -1.], [1., 0.]]) S = np.array([[2., 0.], [0., 2.]]) v = np.array([1., 0.])
print('(R @ S) @ v =', (R @ S) @ v) print('(S @ R) @ v =', (S @ R) @ v) print('same:', np.allclose((R @ S) @ v, (S @ R) @ v))