Optimisation & Mathematical Methods for Economics The University of Edinburgh · School of Economics
This week we move from conditions for optima to algorithms that find them. We implement gradient descent and Newton’s method, compare their convergence, and apply them to economic problems.
1. Gradient Descent
Idea: Start at some \(\mathbf{x}_0\). At each step, move in the direction of steepest descent (negative gradient):
Convergence: Newton’s method has quadratic convergence near the optimum (vs. linear for gradient descent), but each step is more expensive and requires computing/inverting the Hessian.
# Newton's method for a simpler example: f(x,y) = x^4 + y^4 - 4xy + 1def f_simple(x):return x[0]**4+ x[1]**4-4*x[0]*x[1] +1def grad_simple(x):return np.array([4*x[0]**3-4*x[1], 4*x[1]**3-4*x[0]])def hess_simple(x):return np.array([[12*x[0]**2, -4], [-4, 12*x[1]**2]])def newtons_method(grad_f, hess_f, x0, tol=1e-8, max_iter=100): x = x0.copy() path = [x.copy()]for i inrange(max_iter): g = grad_f(x)if np.linalg.norm(g) < tol:break H = hess_f(x) direction = np.linalg.solve(H, -g) x = x + direction path.append(x.copy())return np.array(path)x0 = np.array([2.0, 2.0])# Comparepath_newton = newtons_method(grad_simple, hess_simple, x0)path_gd2 = gradient_descent(grad_simple, x0, alpha=0.01, max_iter=500)print(f"Newton: {len(path_newton)} iterations -> ({path_newton[-1][0]:.6f}, {path_newton[-1][1]:.6f})")print(f"GD: {len(path_gd2)} iterations -> ({path_gd2[-1][0]:.6f}, {path_gd2[-1][1]:.6f})")
# Convergence comparisonfig, ax = plt.subplots(figsize=(8, 5))fvals_newton = [f_simple(p) for p in path_newton]fvals_gd = [f_simple(p) for p in path_gd2]ax.semilogy(range(len(fvals_newton)), fvals_newton, 'r-o', label="Newton's method", markersize=6)ax.semilogy(range(min(50, len(fvals_gd))), fvals_gd[:50], 'b-s', label='Gradient descent', markersize=4)ax.set_xlabel('Iteration'); ax.set_ylabel('$f(x_k)$ (log scale)')ax.set_title('Convergence: Newton vs Gradient Descent')ax.legend(); ax.grid(True, alpha=0.3)plt.tight_layout()plt.show()
3. Quasi-Newton Methods: BFGS
Computing the full Hessian is expensive. BFGS (Broyden–Fletcher–Goldfarb–Shanno) approximates the inverse Hessian using gradient information only, achieving superlinear convergence without second derivatives.
In practice, BFGS (and its limited-memory variant L-BFGS) is the workhorse of smooth optimisation.
Exercise 1: Implement gradient descent for \(f(x,y) = (x-3)^2 + 2(y+1)^2\). Try different step sizes \(\alpha \in \{0.01, 0.1, 0.5, 1.0\}\) and plot the convergence.
def f_quad(x): return (x[0]-3)**2+2*(x[1]+1)**2def grad_quad(x): return np.array([2*(x[0]-3), 4*(x[1]+1)])for alpha in [0.01, 0.1, 0.5, 1.0]: path = gradient_descent(grad_quad, np.array([0.0, 0.0]), alpha=alpha, max_iter=200) fvals = [f_quad(p) for p in path] plt.semilogy(fvals, label=f'alpha={alpha}')plt.legend(); plt.xlabel('Iteration'); plt.ylabel('f(x)')plt.title('Effect of step size'); plt.show()# alpha=0.5 converges fastest; alpha=1.0 diverges (too large)
Exercise 2: Use scipy.optimize.minimize with method=‘Nelder-Mead’ (derivative-free) and compare with BFGS. When might a derivative-free method be useful?
Derivative-free methods are useful when the objective is noisy, non-differentiable, or the gradient is very expensive to compute (e.g., simulation-based models in macroeconomics).