# 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}")Week 5: Deep Equilibrium Networks II — Stochastic Models
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:
- Extend the DEQN framework to models with stochastic productivity shocks
- Use Gauss–Hermite quadrature to compute expectations in the Euler equation
- Compare different loss functions (MSE, Huber, log-cosh) for DEQN training
- Simulate the solved model and compute impulse response functions
1. Adding uncertainty to the growth model
The deterministic Brock–Mirman model from Week 4 is a useful testbed, but real economies have shocks. The stochastic version adds a productivity shock \(z_t\):
\[\max \mathbb{E}_0 \sum_{t=0}^{\infty} \beta^t u(c_t) \quad \text{s.t.} \quad k_{t+1} = z_t k_t^\alpha - c_t\]
where \(\ln z_t = \rho \ln z_{t-1} + \varepsilon_t\), \(\varepsilon_t \sim N(0, \sigma^2)\).
The state space is now two-dimensional: \((k_t, z_t)\). The policy network maps \((k, z) \to c\).
The stochastic Euler equation
\[u'(c_t) = \beta \, \mathbb{E}_t \big[ u'(c_{t+1}) \, \alpha z_{t+1} k_{t+1}^{\alpha-1} \big]\]
The expectation is over tomorrow’s shock \(z_{t+1}\), conditional on today’s \(z_t\).
2. Gauss–Hermite quadrature for expectations
We need to compute \(\mathbb{E}_t[g(z_{t+1})]\) where \(z_{t+1}\) depends on \(z_t\) and a normal shock. Gauss–Hermite quadrature approximates this integral using a weighted sum:
\[\mathbb{E}[g(\varepsilon)] \approx \sum_{j=1}^{Q} w_j \, g(\varepsilon_j)\]
where \((\varepsilon_j, w_j)\) are the quadrature nodes and weights.
# Gauss-Hermite quadrature
from numpy.polynomial.hermite import hermgauss
# 5-point quadrature (sufficient for smooth integrands)
Q = 5
nodes, weights = hermgauss(Q)
# Adjust for standard normal: ε ~ N(0, σ²)
sigma_eps = 0.02
quad_nodes = nodes * np.sqrt(2) * sigma_eps
quad_weights = weights / np.sqrt(np.pi)
print("Quadrature nodes and weights:")
for j in range(Q):
print(f" ε_{j+1} = {quad_nodes[j]:+.4f}, w_{j+1} = {quad_weights[j]:.4f}")
print(f"\nWeights sum to: {quad_weights.sum():.6f} (should be 1.0)")
# Verify: E[ε²] should equal σ²
E_eps2 = np.sum(quad_weights * quad_nodes**2)
print(f"E[ε²] = {E_eps2:.6f}, σ² = {sigma_eps**2:.6f}")3. The stochastic DEQN
The policy network now takes two inputs: \(c = \text{NN}(k, z; \theta)\).
The loss function replaces the deterministic Euler equation with its expectation:
\[\mathcal{L}(\theta) = \mathbb{E}_{k,z} \left[ \left( u'(c_t) - \beta \sum_{j=1}^Q w_j \, u'(c_{t+1}^j) \, \alpha z_{t+1}^j (k')^{\alpha-1} \right)^2 \right]\]
where \(z_{t+1}^j = z_t^\rho \exp(\varepsilon_j)\) for each quadrature node \(j\).
# Stochastic DEQN implementation
RHO = 0.9 # persistence of productivity
SIGMA = 0.02 # std dev of shock
ALPHA = 0.33
BETA = 0.95
# Convert quadrature to torch
quad_nodes_t = torch.tensor(quad_nodes, dtype=torch.float32).to(device)
quad_weights_t = torch.tensor(quad_weights, dtype=torch.float32).to(device)
class StochasticPolicyNet(nn.Module):
"""Maps (k, z) → c, ensuring 0 < c < z·k^α."""
def __init__(self, hidden=64):
super().__init__()
self.net = nn.Sequential(
nn.Linear(2, hidden), nn.Tanh(),
nn.Linear(hidden, hidden), nn.Tanh(),
nn.Linear(hidden, 1)
)
def forward(self, k, z):
x = torch.cat([k, z], dim=1)
raw = self.net(x)
share = torch.sigmoid(raw)
output = z * k ** ALPHA
return share * output * 0.99
def stochastic_euler_loss(policy_net, k, z):
"""Euler-equation loss with stochastic shocks via quadrature."""
c_t = policy_net(k, z)
k_next = z * k ** ALPHA - c_t
# Expected marginal utility tomorrow (quadrature)
rhs = torch.zeros_like(c_t)
for j in range(len(quad_nodes_t)):
ln_z_next = RHO * torch.log(z) + quad_nodes_t[j]
z_next = torch.exp(ln_z_next)
c_next = policy_net(k_next, z_next)
mpk_next = ALPHA * z_next * k_next ** (ALPHA - 1)
rhs += quad_weights_t[j] * BETA * mpk_next / c_next
lhs = 1.0 / c_t
residual = lhs - rhs
return torch.mean(residual ** 2)
print("Stochastic DEQN ready. State space: (k, z) → c")
print(f"Quadrature points: {Q}")4. Loss function design
Different loss functions penalise residuals differently. For economic models, the choice matters:
| Loss | Formula | Behaviour |
|---|---|---|
| MSE | \(r^2\) | Penalises large residuals heavily |
| Huber | \(r^2\) if \(|r| < \delta\), else \(\delta|r|\) | Robust to outliers |
| Log-cosh | \(\ln(\cosh(r))\) | Smooth approximation to Huber |
Recommendation: start with MSE; switch to Huber or log-cosh if training is unstable.
5. Impulse response functions
Once the model is solved, we can simulate it forward and compute IRFs — how the economy responds to a one-time productivity shock.
Exercises
Exercise 1: Train the stochastic DEQN for 10,000 epochs. Plot the policy function \(c(k, z)\) as a surface and compare slices at \(z = 0.95, 1.0, 1.05\).
Exercise 2: Simulate the solved model for 1,000 periods. Plot the time series of \(k_t\), \(c_t\), and \(z_t\). Compute the standard deviation of output.
Exercise 3: Compute impulse response functions: starting from steady state, apply a 1% productivity shock and trace the response of capital and consumption over 40 periods.
Next week: Constraints and Real-World Models — borrowing limits and Fischer–Burmeister complementarity.