Solving tips
- Backprop through a matmul is just two more matmuls: dX = dOut @ W.T and dW = X.T @ dOut. Each factor's shape is forced by the chain rule, so match shapes and the transposes place themselves.
- The bias is added by broadcasting one row across all n examples, so its gradient sums the upstream gradient back down over the batch axis: db = dOut.sum(axis=0).
- Sanity-check every result against the forward shapes: dX must match X, dW must match W, db must match b. If a shape is off, a transpose or a sum axis is wrong.
A dense layer computes Y = X @ W + b, mapping a batch of n inputs of width d_in to n outputs of width d_out. During training you are handed dOut, the gradient of the scalar loss with respect to Y, and you must push it back to the layer’s parameters and to its input. This is the single most reused backprop step in deep learning, and interviewers use it to check whether you can derive matmul gradients from the chain rule and get every shape and transpose right.
Definition
For the forward map Y = X @ W + b with X shape (n, d_in), W shape (d_in, d_out), and bias b shape (d_out,) broadcast across the n rows, the chain rule gives three gradients:
dX = dOut @ W.T # (n, d_out) @ (d_out, d_in) -> (n, d_in)
dW = X.T @ dOut # (d_in, n) @ (n, d_out) -> (d_in, d_out)
db = dOut.sum(axis=0) # sum over the batch -> (d_out,)
Each rule is forced by shape matching. dX must look like X, so the only legal product is dOut @ W.T. dW must look like W, so it must be X.T @ dOut. The bias was added once per example by broadcasting, so its gradient collapses that broadcast by summing dOut down the batch axis.
Task
Complete dense_backward(dOut, X, W) so it returns the tuple (dX, dW, db) using the three rules above, fully vectorized with NO Python loops over examples or features. Use only NumPy matrix products and a sum; do not call any autodiff or framework helper. The returned shapes must be exactly dX: (n, d_in), dW: (d_in, d_out), db: (d_out,).
Example
X = np.array([[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0]]) # (2, 3) -> n=2, d_in=3
W = np.array([[1.0, 0.0],
[0.0, 1.0],
[1.0, 1.0]]) # (3, 2) -> d_out=2
dOut = np.array([[1.0, 1.0],
[1.0, 1.0]]) # (2, 2)
dX, dW, db = dense_backward(dOut, X, W)
# dX -> array([[1., 1., 2.],
# [1., 1., 2.]])
# dW -> array([[5., 5.],
# [7., 7.],
# [9., 9.]])
# db -> array([2., 2.])
dX reads each row of W.T summed by the ones in dOut; dW accumulates each input feature against the upstream gradient over both examples; db sums the two rows of dOut.
Constraints
1 <= n <= 10^4,1 <= d_in, d_out <= 10^3; use vectorized NumPy matmuls, no Python loop over rows or columns.- Inputs are real-valued
float64; no NaN or inf. Return the three gradients in the order(dX, dW, db). dbis a 1-D array of shape(d_out,), not(1, d_out).