Calculus for Machine Learning: Derivatives, Partial Derivatives, and Gradients
Learn derivatives visually, then see how partial derivatives and gradients drive neural-network training, with worked examples and verified Python code.

A neural network learns by answering one question over and over: if I nudge this parameter slightly, does the error go up or down, and by how much? Calculus is the tool that answers it. A derivative measures how fast a function changes. A partial derivative does the same when the function has many inputs and you move only one. The gradient collects all the partial derivatives into one vector that points uphill, so training can step the other way.
This guide builds those three ideas in order, starting with pictures. By the end you will be able to:
- explain a derivative as the slope of a tangent line and as a sensitivity,
- compute partial derivatives and assemble a gradient by hand,
- verify your calculus with a numerical gradient check in Python,
- follow how gradients, the chain rule, and the update rule fit together in training.
You only need comfort with algebra and functions. Everything else is introduced as it appears.
Note on the code. Every NumPy and SymPy example in this article was executed (Python 3.12, NumPy 2.4, SymPy 1.14) and the printed output shown is the real output. The PyTorch snippet in the autograd section was reviewed but not executed; its expected output is the analytic result computed in the NumPy example before it.
The Big Picture in One Table
| Concept | Question it answers | Role in machine learning |
|---|---|---|
| Derivative | How fast does the output change as one input changes? | Sensitivity of the loss to a single parameter |
| Partial derivative | Same question, when there are several inputs and only one moves | Sensitivity of the loss to one weight among many |
| Gradient | Which direction increases the output fastest, and how steeply? | The direction gradient descent moves against |
| Chain rule | How do rates of change combine through stacked functions? | The mathematical core of backpropagation |
Part 1: The Derivative, Visually
From secant lines to a tangent line
Take and look at the point . The slope of a straight line is easy: rise over run. A curve has a different slope at every point, so we approximate. Draw a secant line through and a second point a distance to the right. Its slope is the difference quotient:
Now shrink . The second point slides toward the first, and the secant line rotates until it just grazes the curve. That limiting line is the tangent line, and its slope is the derivative:

For at we can do the algebra exactly:
As the slope approaches . Python agrees:
def f(x):
return x**2
x = 3.0
print(f"{'h':>10} {'(f(x+h)-f(x))/h':>18}")
for h in [1, 0.1, 0.01, 0.001, 0.0001]:
print(f"{h:>10} {(f(x+h)-f(x))/h:>18.6f}")
Output:
h (f(x+h)-f(x))/h
1 7.000000
0.1 6.100000
0.01 6.010000
0.001 6.001000
0.0001 6.000100
Notice the pattern in the right-hand column. The derivative is not magic: it is what a very small secant slope settles on.
The derivative as sensitivity
The slope picture has a second, more useful reading for machine learning. Rearranging the definition gives a first-order approximation for small changes :
Read it as: the output changes by about the derivative times the nudge. Suppose a toy loss is and the current weight is . Then , so .
- The sign is negative, so increasing lowers the loss.
- The size says a nudge of should change the loss by about .
The exact change is . The approximation is close, and the sign told us which way to move. That is the entire idea behind gradient descent, just in one dimension.
Derivative rules you will actually use
You do not need to take limits every time. A short list covers most of what appears in machine learning:
| Rule | Formula |
|---|---|
| Constant multiple | |
| Sum | |
| Power | |
| Product | |
| Chain | |
| Exponential and logarithm | , |
| Sigmoid | |
| Hyperbolic tangent | |
| ReLU | for , for |
ReLU is not differentiable at exactly . Deep learning libraries simply pick a convention there (commonly ), and in practice this rarely matters.
The chain rule is “rates multiply”
Think of three gears. If gear A turns 2 times for every turn of B, and B turns 3 times for every turn of C, then A turns times per turn of C. The chain rule is that statement for functions: when depends on and depends on ,
A neural network is a long chain of such dependencies, which is why this rule matters so much. We will use it in a concrete example shortly.
Part 2: Partial Derivatives
One input at a time
Real models have thousands to billions of parameters, so the loss is a function of many variables. Consider a function of two:
Imagine its graph as a landscape. A partial derivative asks: if I walk exactly parallel to the -axis, how steep is the ground? You freeze at a constant, treat it like a number, and differentiate with respect to only:
In the first formula, became , so its derivative in is and the term vanished as a constant. The curly is a visual reminder that other variables are being held fixed.
At the point :
A quick sanity check using the sensitivity idea: , and nudging by should add roughly . Directly, , a change of .
Verifying with code
Two independent checks are better than one: symbolic differentiation with SymPy, and a numerical central difference, which is more accurate than the one-sided quotient we used earlier because the errors on both sides cancel.
import numpy as np
import sympy as sp
def f(x, y):
return x**2 + 3*x*y + y**2
def numeric_grad(f, point, h=1e-6):
point = np.asarray(point, dtype=float)
grad = np.zeros_like(point)
for i in range(len(point)):
step = np.zeros_like(point)
step[i] = h
grad[i] = (f(*(point + step)) - f(*(point - step))) / (2 * h)
return grad
x, y = sp.symbols("x y")
expr = x**2 + 3*x*y + y**2
grad_sym = [sp.diff(expr, v) for v in (x, y)]
print("Symbolic gradient:", grad_sym)
print("Symbolic at (1,2):", [g.subs({x: 1, y: 2}) for g in grad_sym])
print("Numeric at (1,2):", numeric_grad(f, [1, 2]).round(6))
Output:
Symbolic gradient: [2*x + 3*y, 3*x + 2*y]
Symbolic at (1,2): [8, 7]
Numeric at (1,2): [8. 7.]
The hand calculation, the symbolic result, and the numerical estimate all agree.
Part 3: The Gradient
Collecting partial derivatives into a vector
For a function of variables, the gradient stacks all partial derivatives into one vector:
For our example, . In machine learning the variables are model parameters and the function is the loss , so holds one number per parameter, each saying how sensitive the loss is to that parameter.
What the gradient means geometrically
The gradient is more than a bookkeeping device. It has three properties worth memorizing:
- Direction. It points in the direction of steepest increase of the function.
- Magnitude. Its length, , is that steepest slope.
- Orientation. It is perpendicular to the contour lines (the level sets where is constant).

Why is the gradient the steepest direction? The slope of along a unit direction is the directional derivative
where is the angle between and the gradient. This is largest () when , meaning points along the gradient, and most negative when points directly against it. That is why training moves in the direction .
Part 4: How Gradients Train a Model
The gradient descent update
Training turns the geometry above into a loop. Compute the gradient of the loss at the current parameters, then take a small step downhill:
The scalar is the learning rate. The minus sign is there because the gradient points uphill and we want to go down.
Here is the loop on a bowl-shaped function , whose gradient is , starting from :
import numpy as np
def grad(p):
x, y = p
return np.array([2 * x, 6 * y])
def descend(lr, steps=6, start=(4.0, 2.0)):
p = np.array(start)
path = [p.copy()]
for _ in range(steps):
p = p - lr * grad(p)
path.append(p.copy())
return np.array(path)
for lr in (0.1, 0.4):
print(f"learning rate = {lr}")
for k, p in enumerate(descend(lr)):
print(f" step {k}: x={p[0]:8.4f} y={p[1]:9.4f}")
Output:
learning rate = 0.1
step 0: x= 4.0000 y= 2.0000
step 1: x= 3.2000 y= 0.8000
step 2: x= 2.5600 y= 0.3200
step 3: x= 2.0480 y= 0.1280
step 4: x= 1.6384 y= 0.0512
step 5: x= 1.3107 y= 0.0205
step 6: x= 1.0486 y= 0.0082
learning rate = 0.4
step 0: x= 4.0000 y= 2.0000
step 1: x= 0.8000 y= -2.8000
step 2: x= 0.1600 y= 3.9200
step 3: x= 0.0320 y= -5.4880
step 4: x= 0.0064 y= 7.6832
step 5: x= 0.0013 y= -10.7565
step 6: x= 0.0003 y= 15.0591

With the loss shrinks steadily. With it blows up in the direction even though is converging. The update rule explains exactly why. Each step multiplies by and by :
- : factors and , both shrink.
- : factors and . The coordinate flips sign and grows by 40% each step.
For this function, stability requires , i.e. . The steepest direction limits how large a learning rate you can use, a small version of what makes real loss surfaces hard to optimize.
Deriving the gradient of a linear regression loss
Now a model with data. Fit by minimizing the mean squared error
Apply the chain rule to each squared term. The outer function is “square” (derivative ) and the inner function is (derivative with respect to , and with respect to ):
Whenever you derive a gradient by hand, check it numerically before trusting it. This is called a gradient check:
import numpy as np
rng = np.random.default_rng(0)
x = rng.uniform(-1, 1, size=100)
y = 2.0 * x + 1.0 + rng.normal(0, 0.1, size=100) # true w = 2, b = 1, plus noise
def loss(w, b):
return np.mean((w * x + b - y) ** 2)
def analytic_grad(w, b):
err = w * x + b - y
return np.array([2 * np.mean(err * x), 2 * np.mean(err)])
def numeric_grad(w, b, h=1e-6):
return np.array([
(loss(w + h, b) - loss(w - h, b)) / (2 * h),
(loss(w, b + h) - loss(w, b - h)) / (2 * h),
])
print("analytic:", analytic_grad(0.0, 0.0))
print("numeric: ", numeric_grad(0.0, 0.0))
print("max abs diff:", np.max(np.abs(analytic_grad(0.0, 0.0) - numeric_grad(0.0, 0.0))))
w, b, lr = 0.0, 0.0, 0.1
for step in range(201):
gw, gb = analytic_grad(w, b)
w, b = w - lr * gw, b - lr * gb
if step % 50 == 0:
print(f"step {step:3d} loss={loss(w, b):.4f} w={w:.3f} b={b:.3f}")
Output:
analytic: [-1.69404684 -2.3705008 ]
numeric: [-1.69404685 -2.3705008 ]
max abs diff: 7.216147679400819e-10
step 0 loss=2.1004 w=0.169 b=0.237
step 50 loss=0.0100 w=1.955 b=0.999
step 100 loss=0.0094 w=1.993 b=0.993
step 150 loss=0.0094 w=1.993 b=0.993
step 200 loss=0.0094 w=1.993 b=0.993
The two gradients match to about nine decimal places, so the formulas are right. Training then recovers and , close to the true values and but not identical, because the data contain noise. The loss levels off near , which reflects that noise and is not a bug. These numbers apply to this random seed only.
The chain rule through a single neuron
Backpropagation is the chain rule applied systematically through every layer. Its core is visible in a single sigmoid neuron with squared error. Let
where is the target. Chaining the three local derivatives:
Take , , , . Then , so , and:
- ,
Therefore and . Both are negative, so increasing or would reduce the loss. Verified in code:
import math
def sigmoid(z):
return 1 / (1 + math.exp(-z))
x, t = 2.0, 1.0
def loss(w, b):
a = sigmoid(w * x + b)
return (a - t) ** 2
w, b = 0.5, -1.0
a = sigmoid(w * x + b)
dL_da = 2 * (a - t)
da_dz = a * (1 - a)
dL_dw = dL_da * da_dz * x
dL_db = dL_da * da_dz * 1
print("a =", a)
print("dL/dw =", dL_dw, " dL/db =", dL_db)
h = 1e-6
print("numeric dL/dw =", (loss(w + h, b) - loss(w - h, b)) / (2 * h))
print("numeric dL/db =", (loss(w, b + h) - loss(w, b - h)) / (2 * h))
Output:
a = 0.5
dL/dw = -0.5 dL/db = -0.25
numeric dL/dw = -0.5000000000837668
numeric dL/db = -0.2500000000349445
Letting a framework do it: automatic differentiation
Real networks have too many operations to differentiate by hand. Frameworks use automatic differentiation. According to the PyTorch autograd documentation, autograd is a reverse-mode automatic differentiation system that records operations in a graph during the forward pass and then applies the chain rule while traversing it backward. A survey by Baydin, Pearlmutter, Radul, and Siskind (JMLR, 2018) describes backpropagation as a special case of this family of techniques.
Here is the same neuron in PyTorch. This snippet was reviewed but not executed for this article:
import torch
x = torch.tensor(2.0)
t = torch.tensor(1.0)
w = torch.tensor(0.5, requires_grad=True)
b = torch.tensor(-1.0, requires_grad=True)
a = torch.sigmoid(w * x + b)
loss = (a - t) ** 2
loss.backward()
print(w.grad, b.grad)
# Expected from the analytic result above: tensor(-0.5000) tensor(-0.2500)
Two behaviors from the PyTorch documentation matter in training loops. Only leaf tensors with requires_grad=True receive a .grad, and gradients accumulate across backward() calls, so you must reset them between optimizer steps. Optimizers provide zero_grad() for that purpose.
Common Mistakes and Troubleshooting
- Treating the gradient as a scalar. A derivative is a number for one input. A gradient is a vector with one entry per input.
- Wrong sign. Gradient descent subtracts the gradient. Adding it performs gradient ascent and your loss will rise.
- Learning rate too large. If the loss oscillates or explodes, as in the run above, reduce before suspecting anything else.
- Differentiating a “held-fixed” variable. In partial derivatives, other variables are constants, not zero. The term contributes , not nothing.
- Skipping the gradient check. When a hand-derived gradient disagrees with the numerical one by more than roughly in relative terms, the formula (or the code) is usually wrong.
- Choosing too small in finite differences. Floating-point rounding error grows as shrinks. Values around to with central differences work well for double precision, and values like do not.
- Forgetting to zero gradients in PyTorch training loops (see above).
- Assuming a zero gradient means a minimum. It could be a maximum or a saddle point. The gradient only describes the surface locally.
- Assuming the minibatch gradient is the true gradient. In practice, training usually estimates the gradient from a small batch, so each step is a noisy approximation.
Practice Exercises
Try each before reading the solution.
- Find for .
- Find both partial derivatives of and evaluate them at .
- For , perform one gradient descent step from with .
Solutions
- , so .
- and . At : and , so the gradient is .
- The gradient at is . The new point is , matching step 1 in the output above.
Frequently Asked Questions
What is the difference between a derivative and a partial derivative?
A derivative applies to a function of one variable. A partial derivative applies to a function of several variables and measures the rate of change along one variable while all the others are held constant.
Why does gradient descent subtract the gradient?
The gradient points toward the steepest increase of the loss. Moving the opposite way decreases the loss fastest locally, which is the direction we want when minimizing.
Is backpropagation the same as the chain rule?
Backpropagation is the chain rule organized efficiently. It computes the gradient of a scalar loss with respect to all parameters by working backward through the network and reusing intermediate results. In the language of automatic differentiation, it is reverse-mode differentiation applied to neural networks.
What is the difference between numerical, symbolic, and automatic differentiation?
Numerical differentiation approximates the derivative with finite differences, which is simple but slow for many parameters and subject to rounding error. Symbolic differentiation manipulates formulas to produce an exact expression, as SymPy does. Automatic differentiation computes exact derivatives (up to floating-point precision) of a program by applying the chain rule to its elementary operations, and is what deep learning frameworks use.
How much calculus do I need for machine learning?
For understanding training, the topics in this article are the core: derivatives, the chain rule, partial derivatives, and gradients. Matrix calculus (Jacobians, and Hessians for second-order methods) is a natural next layer. Integrals appear mainly in probability and statistics.
What happens when the gradient is zero?
You are at a stationary point: a minimum, maximum, or saddle point. Gradient descent makes no further progress there. Which one it is requires second-order information such as the Hessian or additional analysis of the loss surface.
Why do I need to check gradients numerically?
Hand-derived gradients and hand-written backward passes are easy to get subtly wrong, and training can still appear to run with incorrect gradients. A central-difference check on a few parameters is a cheap way to catch those errors.
Conclusion
A derivative is the slope you get when a secant line shrinks to a tangent, and it tells you how sensitive an output is to a small nudge. Partial derivatives extend that idea to many inputs by freezing all but one. The gradient assembles them into the uphill direction, and gradient descent walks the opposite way. The chain rule connects the layers, and automatic differentiation applies it for you.
A good next step is to work through backpropagation on a two-layer network by hand, comparing each gradient against a numerical check as we did here. After that, extend this article’s ideas to vectors and matrices with Jacobians and the Hessian, then explore how optimizers such as momentum and Adam modify the plain update rule.
Sources and Further Reading
- PyTorch documentation, Autograd mechanics.
- Atılım Güneş Baydin, Barak A. Pearlmutter, Alexey Andreyevich Radul, Jeffrey Mark Siskind. Automatic Differentiation in Machine Learning: a Survey. Journal of Machine Learning Research, 18(153):1–43, 2018.
- Marc Peter Deisenroth, A. Aldo Faisal, Cheng Soon Ong. Mathematics for Machine Learning. Cambridge University Press, 2020. A free-to-read reference covering vector calculus and optimization in more depth.



