Calculus and optimization
A lidar returns 60 points along a wall, each a little off. Where exactly is the wall? A neural network that picks up objects has a million weights. Which way should each one move to make it fail less? A robot arm needs its hand at a given spot. Which joint changes get it there? These are all the same question: find the values that make an error as small as possible. The tools are derivatives and a loop that follows them downhill.
You will be able to
- Explain a derivative and a gradient as the local slope and the direction of steepest increase.
- Verify an analytic gradient against a numerical one.
- Fit a line to noisy points with least squares, both in closed form and with NumPy.
- Implement gradient descent and see what a bad learning rate does.
- Read a Jacobian as the derivative of a robot's motion.
Derivatives: how fast does it change?
The derivativeA derivative measures how quickly a quantity changes when you nudge its input; it is the slope of the curve at a point. of a function tells you how much changes for a tiny nudge in . If , then , so at the function climbs at a rate of 7 per unit.
You do not always have a formula. A computer can estimate the slope by nudging the input both ways and dividing:
for a small . This central difference is accurate and takes three lines.
def f(x):
return x ** 2 + 3 * x
def num_grad(f, x, h=1e-6):
return (f(x + h) - f(x - h)) / (2 * h)
print(num_grad(f, 2.0)) # 7.0000000001, matches 2*2 + 3
Numerical gradients are slow for large problems, but they are the best way to test the formula you derived by hand. If your analytic derivative and the numerical one disagree, your formula is wrong. Professional robotics and machine learning engineers do this check constantly.
Gradients: slope in many directions
Most robotics functions have many inputs. A gradientThe gradient of a function of several variables is the vector of its partial derivatives; it points in the direction of steepest increase. collects the slope along each input into one vector:
The gradient points uphill, in the direction where grows fastest. If you want to make smaller, step the other way. That is the entire idea of gradient descentGradient descent minimises a function by repeatedly stepping in the direction opposite to its gradient.:
The number is the learning rate, the step size. Too small and you crawl. Too large and you overshoot and blow up. You will see both.
A running example: fit a line to noisy data
A lidar sees points along a straight wall. We model the wall as and want the best and . Each point leaves a residual: the gap between what the line predicts and what was measured. The best line makes the total squared residual smallest:
This is least squares. Squaring makes every error positive and punishes large ones more than small ones. It is also the maximum-likelihood answer when noise is Gaussian, which links straight back to the last lesson.
Write the model as a matrix equation. Stack the data into (a column of x values and a column of ones) and , so the prediction is and
Setting the gradient to zero gives the normal equations, , which you can solve directly.
import numpy as np
rng = np.random.default_rng(3)
m_true, b_true = 0.8, 1.5
x = np.linspace(0.0, 5.0, 60)
y = m_true * x + b_true + rng.normal(0.0, 0.1, size=x.size)
A = np.column_stack([x, np.ones_like(x)]) # shape (60, 2)
# 1. Library least squares (the one to use in practice)
theta_ls, *_ = np.linalg.lstsq(A, y, rcond=None)
print(theta_ls) # about [0.80 1.50]
# 2. Normal equations, solved directly
theta_ne = np.linalg.solve(A.T @ A, A.T @ y)
print(theta_ne) # same numbers
# 3. Gradient descent
def loss(theta):
r = A @ theta - y
return (r @ r) / len(y)
def grad(theta):
r = A @ theta - y
return 2.0 * A.T @ r / len(y)
theta = np.zeros(2)
lr = 0.02
for _ in range(5000):
theta -= lr * grad(theta)
print(theta, loss(theta)) # lands on the same answer
All three give the same line, about slope 0.80 and intercept 1.50. For a linear model like this one, lstsq is the right tool: exact, fast and numerically careful. Gradient descent earns its keep when there is no closed form, as with neural networks.
Check your gradient
Before trusting a hand-derived gradient, compare it with a numerical one.
# continues from fit_line.py
t0 = np.array([0.3, -0.2])
h = 1e-6
num = np.array([(loss(t0 + e) - loss(t0 - e)) / (2 * h) for e in np.eye(2) * h])
print(num) # [-16.90 -5.90]
print(grad(t0)) # the same, to about 6 digits
If these disagree, find the bug in grad before you do anything else.
What a bad learning rate does
# continues from fit_line.py
theta = np.zeros(2)
for _ in range(50):
theta -= 0.2 * grad(theta) # too large
print(theta) # huge numbers: the iteration diverged
With the step size 0.2, each step overshoots the valley, lands higher up the opposite wall, and the error grows until the numbers explode. With 0.02 it converged. Picking the learning rate is a practical skill, and a diverging loss is the usual sign it is too high.
Because errors are squared, a single wild point has an enormous pull. Add two bad lidar returns to the data above (one 8 m too high, one 6 m too low) and the fitted line moves to roughly slope 0.66 and intercept 1.89, far from the truth. Real sensors produce such outliers. The fix, robust estimation such as RANSAC, comes in Module 05.
Jacobians: calculus for moving robots
A robot arm's hand position depends on its joint angles. Suppose a two-link planar arm has link lengths and joint angles . The hand is at
How does the hand move when the joints move a little? The matrix of all partial derivatives is the JacobianThe Jacobian is the matrix of partial derivatives that says how small changes in the inputs of a function change its outputs. , and it connects the two:
To reach a target, you can use this like gradient descent: measure the hand's error, convert it to a joint change with the (pseudo-)inverse of , and repeat. The check with a numerical Jacobian works exactly as before.
import numpy as np
def fk(q, l1=1.0, l2=0.8):
return np.array([l1 * np.cos(q[0]) + l2 * np.cos(q[0] + q[1]),
l1 * np.sin(q[0]) + l2 * np.sin(q[0] + q[1])])
def jac(q, l1=1.0, l2=0.8):
s1, c1 = np.sin(q[0]), np.cos(q[0])
s12, c12 = np.sin(q[0] + q[1]), np.cos(q[0] + q[1])
return np.array([[-l1 * s1 - l2 * s12, -l2 * s12],
[l1 * c1 + l2 * c12, l2 * c12]])
q = np.array([0.4, 0.9])
h = 1e-6
num = np.column_stack([(fk(q + e) - fk(q - e)) / (2 * h) for e in np.eye(2) * h])
print(np.allclose(num, jac(q), atol=1e-6)) # True
Do not worry about the arm yet. Module 02 develops forward and inverse kinematics properly. The point here is that the same habits, derive, then verify numerically, carry over.
Hands-on lab
You will fit lidar wall data three ways and compare.
- In
kinemata-workspace/foundations, createfit.py. Generate 80 points along a wall usingy = 0.5 * x + 2.0forxfrom 0 to 8, with Gaussian noise of standard deviation 0.05. - Fit with
np.linalg.lstsqand print the slope and intercept. They should be near 0.5 and 2.0. - Fit again with gradient descent, using
lr = 0.01and 10,000 iterations. Print the result. - Compare the two fits using
np.allclose(theta_gd, theta_ls, atol=1e-2). It should printTrue. If it does not, increase the iterations. - Check your gradient: compare
gradwith a central difference at a randomtheta, withnp.allclose(num, grad(t), atol=1e-4). - Replace one point with
y + 5and refit. Note how far the line moves. - Commit
fit.pywith a message that says what it does.
The check: steps 2, 3 and 4 agree, the gradient check prints True, and the outlier visibly drags the line.
Exercises
Analytic versus numericalBeginner
Derive the derivative of and check it at with By the product rule, . In code: They agree to many decimals.num_grad.Show solutionHide solution
Find the stable learning rateIntermediate
Minimise with gradient descent starting at . Try learning rates 0.1, 0.9, 1.0 and 1.1 for 20 steps. What happens to each? The gradient is , so each step multiplies by . For 0.1 the factor is 0.8, so shrinks smoothly. For 0.9 the factor is : it zigzags across zero but still shrinks. For 1.0 the factor is , so bounces between 5 and -5 forever. For 1.1 the factor is and grows, so it diverges. Descent converges only if .Show solutionHide solution
Fit a circle by least squaresAdvanced
A set of points lies roughly on a circle. The equation is linear in the unknowns , , , where the centre is and the radius is . Fit a circle to the points generated from centre , radius 3 with a little noise. The trick is to rewrite a nonlinear shape so that the unknowns appear linearly, which turns the problem into ordinary least squares.Show solutionHide solution
Check your understanding
Takeaways
- A derivative is local slope, and a gradient is the vector of slopes that points uphill.
- Gradient descent steps against the gradient; the learning rate sets the step size and a too-large one diverges.
- Least squares fits a model by minimising squared residuals, and for linear models
np.linalg.lstsqsolves it exactly. - Always verify a hand-derived gradient with a numerical central difference.
- Squared error is sensitive to outliers, so real systems add robust methods.
- A Jacobian is the derivative of a robot's motion, and it links joint changes to hand changes.
Go deeper
- 3Blue1Brown: Essence of Calculus: visual intuition for derivatives and the chain rule.
- NumPy lstsq reference: details and options of the least-squares solver.
- Modern Robotics, chapter 6: inverse kinematics: a preview of Jacobian-based methods from Module 02.
- Convex Optimization (Boyd and Vandenberghe): the free standard text, for later reading.
Next: Linux, Git and tooling
Finished the lab and the quiz?
Mark it done to track your progress through the course.