Expanded PseudocodePseudocode for K-BFGS
Below is an expandeda basic idea, based on Python, for implementation of K-BFGS for a feedforward neural network, where each layer-wise update maintains positive definiteness and controls step size through damping.
import numpy as np
# Initialise Hessian approximation as an identity matrix for each layer
def initialise_hessian_approx(weights: dict) -> dict:
"""Initialises the Hessian approximation as identity matrices for each layer."""
return {layer: (np.eye(weights[layer].shape[0]), np.eye(weights[layer].shape[1])) for layer in weights}
# BFGS update rule for inverse Hessian approximation
def update_bfgs(A: np.ndarray, G: np.ndarray, s: np.ndarray, y: np.ndarray, damping: float) -> np.ndarray:
"""
Applies the BFGS formula with damping to update the Hessian approximation.
Parameters:
A (np.ndarray): Kronecker factor matrix for layer weights (2D array).
G (np.ndarray): Kronecker factor matrix for layer gradient updates (2D array).
s (np.ndarray): Update direction based on factor A (1D array).
y (np.ndarray): Update direction based on factor G (1D array).
damping (float): Damping term to stabilise updates.
Returns:
np.ndarray: Updated Hessian approximation for the layer (2D array).
Note:
Includes a small constant (1e-8) in the denominator to avoid division by zero.
"""
# Compute scaling factor to adjust the step size based on vectors s and y
rho = 1.0 / (y.T @ s + damping + 1e-8)
I = np.eye(A.shape[0])
V = I - rho * np.outer(y, s)
H_new = V @ A @ V.T + rho * np.outer(s, s)
return H_new
# Expanded K-BFGS update function with damping and initialisation
def kbfgs_update(weights: dict, grads: dict, hessian_approx: dict, learning_rate: float, damping: float = 1e-4):
"""Performs a K-BFGS update step on network weights using layer-wise Hessian approximations."""
for layer in weights:
A, G = hessian_approx[layer] # Kronecker factors representing layer-wise Hessian approximation
# Compute update directions based on Kronecker factors
s = A @ grads[layer]
y = G @ grads[layer]
# Update the inverse Hessian approximation using the BFGS formula
hessian_approx[layer] = update_bfgs(A, G, s, y, damping)
# Compute the update direction and apply it to the weights
update_direction = hessian_approx[layer] @ grads[layer]
weights[layer] -= learning_rate * update_direction
# Example usage
weights = {'layer1': np.random.rand(3, 3), 'layer2': np.random.rand(4, 4)}
grads = {'layer1': np.random.rand(3, 3), 'layer2': np.random.rand(4, 4)}
hessian_approx = initialise_hessian_approx(weights)
learning_rate = 0.01
damping = 1e-4
# Perform a single update step
kbfgs_update(weights, grads, hessian_approx, learning_rate, damping)
Addressing the additional questions:
The only equation I've found for the second order derivatives is in the Wikipedia article, https://en.wikipedia.org/wiki/Backpropagation#Second-order_gradient_descent. There they have the following (with variables names changed to match what I've used so far) $$\dfrac{\partial^2 L}{\partial h_{j_1}^{(l)} \partial h_{j_2}^{(l)}} = \displaystyle \sum_{j_1 j_2} W_{i_1j_1} W_{i_2j_2} \dfrac{\partial^2L}{\partial a_{i_1}^{(l+1)} \partial a_{i_2}^{(l+1)}}$$
Can someone explain where this comes from? Also, I'm confused about the summation indices in this equation - it seems like we already chose particular $j_1$ and $j_2$ values in the LHS.
The equation from Wikipedia represents the Hessian of the loss function $L$ with respect to the post-activation values $h^{(l)}$ at layer 𝑙 l. It’s derived by applying the chain rule for second derivatives, as the dependencies cascade back through each layer of the network.
The term $\dfrac{\partial^2 L}{\partial h_{j_1}^{(l)} \partial h_{j_2}^{(l)}}$ on the left-hand side represents the Hessian with respect to the post-activation values at layer đť‘™ l, while the sum on the right-hand side expresses how this quantity depends on the pre-activation values $a^{(l+1)}$ in the following layer. Each $W_{i_1j_1}$ and $W_{i_2j_2}$ term represents the weight connections from neurons in layer $l$ to neurons in layer $l+1$, capturing how variations in $h^{(l)}$ propagate forward.
The summation over $j_1$ and $j_2$ accounts for all neurons in the previous layer, accumulating their influence on the second derivative of the loss. Although we specify particular $j_1$ and $j_2$ values on the left-hand side, the sum over these indices captures all relevant connections between layers.