We use torch because it computes gradients for us. We write down the loss, and backward() gives the partial derivatives with respect to every parameter.
The algorithm
import numpy as npimport torchimport matplotlib.pyplot as plttorch.manual_seed(6)def gradient_descent(loss, x, eta, T, track=False): path = []for t inrange(T): loss(x).backward()with torch.no_grad(): x -= eta * x.grad x.grad.zero_()if track: path.append(x.detach().clone().numpy())return (x, np.array(path)) if track else x
1
Compute the gradient of the loss with respect to every tensor that has requires_grad=True.
2
Take a step against the gradient.
3
Reset the gradient. torch accumulates gradients, so without this the next step would use the sum of all previous gradients.
The two agree. Gradient descent found the same answer as the closed form, just more slowly.
The path of gradient descent on the loss surface, dark where the loss is small. Move the learning rate and the number of steps. Below about 0.9 it converges, above 1 it diverges.
The learning rate
If the learning rate is too small, we need many steps. If it is too large, the loss goes up instead of down. There is no formula for the right value. We try a few and look at the learning curve.
The negative log-likelihood of the Bernoulli model. ~y negates the boolean tensor.
gradient descent: [-0.063 1.478]
The generator used \(-0.1\) and \(1.7\). With 500 points we get close.
Convex and non-convex
The loss of linear and logistic regression is convex. It has one minimum, and gradient descent finds it from any starting point.
The loss of a neural network is not convex. It has many local minima and many saddle points. Gradient descent finds one of them, and which one depends on where we started.
The same algorithm on a logistic regression. The loss is convex here too, so every starting point ends in the same place, but the path and the number of steps depend on where we begin.
In practice this matters less than it sounds. In high dimensions most local minima of a large network give a similar loss. But it does mean that two runs with different initial values give different models, and that we have to set a seed if we want to reproduce a result.
The same algorithm on a model that is not linear in its parameters. Change the seed and watch two runs end in visibly different fits, at visibly different losses.