The problem in brief
Two small tasks that sit at the heart of training neural networks. First, differentiate the hyperbolic tangent , with reasoning. Second, write a NumPy function that returns the gradient of the mean squared error of a linear model,
with respect to , for X of shape , y of shape and theta of shape . The coding rules are strict: no extra imports, no autograd, nothing from np.linalg, no @, no .T, and no loop. This is Problem 4 of the 2026 USA AI Olympiad Round 1.
How you are scored
5 points for the derivative (the reasoning is required), 15 points for the function, and 0 for the function if any rule is broken. No official solution is published with the problem PDF. We checked both answers numerically: the derivative against central finite differences on (largest gap ), and the gradient against finite differences () and against the textbook matrix formula ().
Question by question
4.1 The derivative of tanh
Write and . Differentiating swaps them: and . By the quotient rule,
Since , the same result can also be written as
Both forms are worth knowing. The first, , is the one used in backpropagation: the forward pass has already computed , so the backward pass gets the local gradient for free, without any new exponential.

The picture explains why deep tanh networks were hard to train. The slope is at most 1 (at ) and drops below 0.03 once . Backpropagation multiplies these factors layer after layer, so saturated units let almost no gradient through: the vanishing gradient problem that ReLU and careful initialisation later fixed.
4.2 The vectorised gradient
The maths. Let be the residual of sample . By the chain rule, . Averaging over the samples,
In matrix form this is , but @ and .T are forbidden. That rule is the real exercise: we have to express both products with broadcasting and reductions.
- is, for each row , .
X * thetabroadcaststheta(shape ) over the rows, andnp.sum(..., axis=1)sums each row. That gives the predictions, shape . - is, for each column , .
residual[:, None]has shape , soX * residual[:, None]scales row by , andnp.mean(..., axis=0)averages down the rows, which already contains the .
import numpy as np
def my_fun_derivative(X, y, theta):
residual = y - np.sum(X * theta, axis=1) # (N,)
return -2.0 * np.mean(X * residual[:, None], axis=0) # (d,)
It uses no loop, no @, no .T and no np.linalg, and nothing is imported besides NumPy. The output has shape as required. A one-line sanity check by hand: with , , , , the residual is and the gradient is , which the function returns.
Common pitfalls.
- Forgetting the factor 2, or the minus sign that comes from differentiating (the gradient then points uphill).
- Writing
X * residualwithout[:, None]: shapes and only broadcast if , and then the result is silently wrong. Keep axes explicit. np.dotornp.matmulare not literally@, but they are matrix products and break the spirit of the rule; the broadcasting version is the safe answer.
What to remember
- : the derivative is written in terms of the function's own output, which is exactly what backpropagation needs.
- Saturating activations have tiny gradients in their tails: that is the root of vanishing gradients.
- The MSE gradient is "minus twice the average of residual times input": .
- Any matrix product can be rebuilt from broadcasting plus a sum over the shared axis. Thinking in terms of shapes, , is the key skill.
- Always check a hand-derived gradient against finite differences.