The same two arrays, multiplied two ways. Look at the shapes as well as the numbers.
import numpy as np features = np.array([[1, 2], [3, 4]]) weights = np.array([10, 100]) features * weights # -> [[10, 200], [30, 400]] elementwise, 2x2 # each row multiplied by the weights, nothing summed features @ weights # -> [210, 430] one value per row # 1*10 + 2*100 = 210, and 3*10 + 4*100 = 430 # @ pairs the LAST dimension of the left with the first of the right: # (2, 2) @ (2,) -> (2,) the 2s meet and disappear np.array([[1, 2, 3]]) @ np.array([[1, 2, 3]]) # ValueError: mismatch in core dimension — 3 does not meet 1
- * broadcasts, @ contracts
The star lines the shapes up and multiplies position by position, keeping the shape. The at sign pairs the last dimension of the left operand with the first of the right, multiplies along it and *sums*, so that dimension disappears from the result. Summing is the whole difference: it is what turns two features into one prediction.
- The inner dimensions meet, and the outer ones survive
(n, f) @ (f,) gives (n,): the f in the middle is what the two arrays agree about, and it is gone from the answer. Reading a shape error is a matter of writing the two shapes side by side and checking whether the inner numbers match — which is also how you work out what shape you should have passed.
- The star version is wrong in a way that looks right
features * weights returns a (2, 2) array of sensible-looking numbers, and if the next step sums along an axis you might even get the right answer by accident. Nothing raises, so the mistake survives until somebody checks the shape — which is the argument for asserting the shape of a prediction rather than only its values.
One operator does the whole thing, and it is not the star. Check the widths agree first, so the error can talk about features.
features @ weights pairs the columns of the matrix with the weights and sums, giving one value per row. features.shape[1] is the number of columns and weights.shape[0] the number of weights.
Compare the widths explicitly rather than relying on numpy’s message, which talks about core dimensions and signatures. Everything else is one line — no loop, no sum, and no axis argument, because the summing is what @ is for.
import numpy as np def predict(features, weights): if features.shape[____] != weights.shape[0]: raise ____( f'{features.shape[1]} features but {weights.shape[0]} weights' ) return features ____ weights
import numpy as np
def predict(features, weights):
# Checked here so the message is about features and weights rather
# than about matmul's core dimensions.
if features.shape[1] != weights.shape[0]:
raise ValueError(
f"{features.shape[1]} features but {weights.shape[0]} weights"
)
# @ pairs the columns with the weights and sums along them, so the
# feature dimension disappears and one prediction per row is left.
return features @ weights
features * weights is legal, broadcasts happily, and returns an (n, f) array instead of an (n,) one — so the mistake is not caught by the arithmetic but by the shape, possibly several functions later. Asserting the shape of a prediction is cheap insurance, and it is why one of the cases here checks the shape rather than the values.
numpy would raise anyway, with a message about a mismatch in core dimension 0 and a signature written as (n?,k),(k,m?)->(n?,m?) — accurate and no use at three in the morning. '2 features but 3 weights' names the two numbers somebody has to reconcile. The same argument as the port parser: the type was already right, the message was not.
The three do the same thing for 2-D inputs and diverge for higher dimensions — dot does something different with stacks of matrices, and @ is matmul. Since PEP 465 gave numpy a dedicated operator, @ is the one to use: it reads as an operation rather than a function call, which matters when a formula has three of them in a row.
If you need a bias term, this is not the whole model — features @ weights + bias is, and the usual trick is a column of ones so the bias is just another weight. For anything beyond a linear combination use a library: sklearn’s LinearRegression fits the weights, which is the part this function assumes somebody else did. And for very large matrices the shape of the multiply matters more than the operator — a chain of them should be ordered to keep the intermediate results small, which np.linalg.multi_dot will work out for you.
- Add a bias by appending a column of ones to the features, and check the prediction matches features @ weights + bias.
- Try features * weights on the two-sample case and look at the shape, then at what (features * weights).sum(axis=1) gives — and why that is the long way round.
- Multiply two 2-D matrices instead of a matrix and a vector, and work out from the shapes which dimension disappears.