matmul versus elementwise multiply
What it is
A @ B (equivalently np.matmul) is the matrix product: the last axis of A contracts with the second-to-last of B. A * B is element-wise and broadcasts.
Why it matters
They are different operations that accept overlapping shapes, so confusing them is a wrong answer rather than an error — A * B on two (n, n) matrices runs happily and means nothing you wanted.
In the real world: Multiplying a customers-by-products purchase matrix by a products-by-price vector gives each customer's total spend in one step.
Visual example
matmulA matrix product contracts the inner axes: (2, 3) @ (3, 2) gives (2, 2), one dot product per output cell.
Cell [0, 0], and its neighbours
[0,0] = 1*1 + 0*3 + 2*1 = 3
[0,1] = 1*2 + 0*0 + 2*1 = 4
[1,0] = 0*1 + 1*3 + 1*1 = 4
[1,1] = 0*2 + 1*0 + 1*1 = 1
Why this matters
Every scoring step — queries against documents, activations against weights — is one matmul, handed to a tuned BLAS kernel that no hand-written loop will approach.
Common misconception
Wrong idea: A * B multiplies matrices.
Correct idea: * is element-wise; @ is the matrix product. The shape rule differs too: @ needs the inner axes to agree, not the whole shape.
- 1Check the inner axes
(2, 3) @ (3, 2): the 3s must agree and then vanish. - 2One dot product per cellOutput
[i, j]is rowiof A against columnjof B. - 3Read the shapeWhat is left is the outer axes:
(2, 2).
A = np.arange(6).reshape(2, 3)
B = np.arange(6).reshape(3, 2)
(A @ B).shape # (2, 2) — (2, 3) x (3, 2)
(A * A).shape # (2, 3) — element-wise
A @ A # raises: (2, 3) x (2, 3) does not contractUsing np.dot on arrays with more than two axes. For 2-D it matches @, but for higher dimensions it contracts differently from matmul, and the difference appears as a batch axis in the wrong place. Use @ and mean it.