Sigmoid Derivative was elementwise: each output only depends on its own input, so σ′(zi) is a single number per element. Softmax is fundamentally different — every output si=softmax(z)i depends on all of z (through the shared normalizing sum), so its derivative isn't a list of numbers, it's a full n×nJacobian matrix: ∂zj∂si=si(δij−sj), where δij is 1 if i=j else 0.
Explicitly building that n×n matrix during backprop would be wasteful — real deep learning frameworks never materialize it. Instead they compute the vector-Jacobian product (VJP) directly: given the upstream gradient ∂s∂L (how loss changes with each softmax output), get ∂z∂L (how loss changes with each softmax input) in one O(n) pass, using an identity that falls out of the Jacobian formula above:
∂zj∂L=sj(∂sj∂L−∑i∂si∂Lsi)
Your task: implement softmax_gradient(s, grad_output), where s is a softmax output (already computed, sums to 1) and grad_output is ∂L/∂s. Return ∂L/∂z, the same length as s.
Implement it yourself
assert softmax_gradient([0.5, 0.5], [1, 0]) == [0.25, -0.25]
assert softmax_gradient([1 / 3, 1 / 3, 1 / 3], [1, 1, 1]) == [0.0, 0.0, 0.0]
assert all(abs(a - b) < 1e-9 for a, b in zip(softmax_gradient([0.7, 0.2, 0.1], [1, 0, 0]), [0.7 * (1 - 0.7), 0.2 * (0 - 0.7), 0.1 * (0 - 0.7)]))
assert all(abs(a - b) < 1e-9 for a, b in zip(softmax_gradient([0.9, 0.1], [0, 1]), [-0.09, 0.09]))