Week 3: Automatic Differentiation for Economics

Deep Learning for Macroeconomics — Honours, The University of Edinburgh
Instructor: Juan Zurita · juan.zurita@ed.ac.uk


Learning objectives

By the end of this notebook you will be able to:

  1. Explain the difference between numerical, symbolic, and automatic differentiation
  2. Use PyTorch’s autograd to compute gradients, Jacobians, and Hessians
  3. Apply autodiff to compute marginal utilities, Euler-equation residuals, and other economic quantities
  4. Understand why autodiff is the key enabler for DEQNs and PINNs

# Setup
import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn

torch.manual_seed(42)
np.random.seed(42)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f"PyTorch {torch.__version__} on {device}")

1. Three ways to differentiate

When you solve economic models, you constantly need derivatives: marginal utility \(u'(c)\), marginal product \(f'(k)\), Jacobians of equilibrium systems.

Method How it works Pros Cons
Numerical \((f(x+h) - f(x-h)) / 2h\) Simple Slow, imprecise, scales badly
Symbolic Algebraic rules (like Mathematica) Exact formulas Exponential expression growth
Automatic Chain rule on computation graph Exact, fast, scales Requires framework support

Automatic differentiation (autodiff) is what makes DEQNs possible. It computes exact derivatives at machine precision, automatically, for any function you can code.

# Comparing the three methods

# Target: d/dx [sin(x²) · exp(-x)] at x = 1.5

import torch

x_val = 1.5

# 1. Analytical (by hand)
# d/dx [sin(x²) exp(-x)] = [2x cos(x²) - sin(x²)] exp(-x)
analytical = (2 * x_val * np.cos(x_val**2) - np.sin(x_val**2)) * np.exp(-x_val)

# 2. Numerical (finite differences)
h = 1e-7
f = lambda x: np.sin(x**2) * np.exp(-x)
numerical = (f(x_val + h) - f(x_val - h)) / (2 * h)

# 3. Automatic (PyTorch)
x = torch.tensor(x_val, requires_grad=True)
y = torch.sin(x**2) * torch.exp(-x)
y.backward()
autodiff = x.grad.item()

print(f"Analytical:  {analytical:.12f}")
print(f"Numerical:   {numerical:.12f}  (error: {abs(numerical - analytical):.2e})")
print(f"Autodiff:    {autodiff:.12f}  (error: {abs(autodiff - analytical):.2e})")
print(f"\n→ Autodiff matches the analytical result to machine precision.")

2. PyTorch autograd in depth

Computing gradients

torch.autograd.grad() gives you fine-grained control — important for computing Euler-equation residuals where you need \(\partial c / \partial k\).

# Computing economic derivatives with autograd

# Marginal utility with CRRA: u'(c) = c^(-γ)
gamma = 2.0
c = torch.tensor(1.5, requires_grad=True)
u = c ** (1 - gamma) / (1 - gamma)  # CRRA utility
u_prime = torch.autograd.grad(u, c, create_graph=True)[0]

print(f"u(c) = c^(1-γ)/(1-γ) at c = {c.item()}")
print(f"u'(c) = c^(-γ) = {u_prime.item():.6f}")
print(f"Check:  c^(-γ) = {c.item()**(-gamma):.6f}")

# Second derivative (for risk aversion)
u_double_prime = torch.autograd.grad(u_prime, c)[0]
print(f"\nu''(c) = -γ c^(-γ-1) = {u_double_prime.item():.6f}")
print(f"Check:  -γ c^(-γ-1) = {-gamma * c.item()**(-gamma-1):.6f}")

# Arrow-Pratt coefficient of absolute risk aversion
ara = -u_double_prime / u_prime
print(f"\nArrow-Pratt ARA = -u''(c)/u'(c) = {ara.item():.6f}")
print(f"Check:  γ/c = {gamma / c.item():.6f}")

3. Jacobians and Hessians

For multi-dimensional models (multiple state variables, multiple policy functions), we need Jacobians and Hessians:

# Jacobian of a 2-equation system

def equilibrium_system(x):
    """A toy 2-equation system: market clearing + Euler equation."""
    k, c = x[0], x[1]
    eq1 = k**0.33 - c - (k - 0.5)  # goods market clearing
    eq2 = c**(-2) - 0.95 * 0.33 * k**(-0.67) * c**(-2)  # Euler equation
    return torch.stack([eq1, eq2])

x = torch.tensor([1.0, 0.5], requires_grad=True)
F = equilibrium_system(x)

# Compute Jacobian
J = torch.zeros(2, 2)
for i in range(2):
    grad = torch.autograd.grad(F[i], x, retain_graph=True)[0]
    J[i] = grad

print("Jacobian of the equilibrium system:")
print(J.numpy().round(4))
print(f"\nDeterminant: {torch.det(J).item():.4f}")
print("(Non-zero determinant → system has a locally unique solution)")

4. Why autodiff matters for economic models

Three applications that are central to this course:

4.1 Euler-equation residuals (Weeks 4–6)

The DEQN loss requires differentiating through the policy network and through the model equations. Autodiff does both simultaneously.

4.2 Physics-informed neural networks (Week 7)

PINNs solve PDEs by computing \(\frac{\partial^2 V}{\partial k^2}\) and \(\frac{\partial V}{\partial t}\) directly from the network — autodiff gives these for free.

4.3 Comparative statics

Given a solved model, autodiff computes \(\frac{\partial c^*}{\partial \alpha}\) (how the optimal policy changes with parameters) without resolving the model.


Exercises

Exercise 1: Compute the gradient, Hessian, and Laplacian of \(f(x, y) = x^2 y + \sin(xy)\) at \((1, 2)\) using PyTorch autograd.

Exercise 2: Write a function that takes a neural network and computes its Jacobian at a given input point using torch.autograd.functional.jacobian(). Apply it to the policy network from Week 1.


Next week: Deep Equilibrium Networks I — solving the growth model without knowing the answer.