Skip to content
AI360Xpert
Beta

Actor Critic using Kronecker-Factored Trust Region (ACKTR)

ACKTR scales natural policy gradients to deep networks by approximating the massive Fisher Information Matrix as Kronecker products of small activation and gradient covariance matrices.

ACKTR decomposes the intractable network-wide Fisher information matrix into compact Kronecker factor matrices for activations and gradients, enabling fast second-order updates.
ACKTR decomposes the intractable network-wide Fisher information matrix into compact Kronecker factor matrices for activations and gradients, enabling fast second-order updates.

Why Does This Exist?

Standard first-order policy gradient methods, like Advantage Actor-Critic (A2C), update neural network parameters along the steepest Euclidean ascent direction. Because parameters govern policy distributions non-linearly, a uniform step in weight space can collapse the policy distribution from exploratory to near-deterministic in a single update.

Natural policy gradients solve this instability by measuring distance in probability distribution space via the Fisher Information Matrix (FIM), denoted FF. The natural gradient direction ∇~θJ=F−1∇θJ\tilde{\nabla}_\theta J = F^{-1} \nabla_\theta J guarantees that step sizes represent invariant Kullback-Leibler (KL) divergences regardless of network parameterization.

However, for a network with D≈106D \approx 10^6 parameters, the FIM has dimension D×DD \times D (101210^{12} elements). Directly storing and inverting FF demands O(D3)≈1018O(D^3) \approx 10^{18} floating-point operations per step, which is computationally impossible. While Trust Region Policy Optimization (TRPO) avoids constructing FF by approximating F−1gF^{-1} g with iterative Conjugate Gradient steps, it requires repeated Hessian-vector passes and only optimizes the actor, leaving the critic to first-order Adam or RMSprop.

Actor Critic using Kronecker-Factored Trust Region (ACKTR) was introduced by Wu et al. (2017) to make exact second-order optimization practical for both the actor and the critic. By applying Kronecker-factored approximate curvature (K-FAC), ACKTR decomposes each layer's Fisher block into two compact covariance matrices: one over input activations and one over output pre-activation gradients. Inverting these tiny matrices reduces computational complexity from O((dindout)3)O((d_{\text{in}} d_{\text{out}})^3) to O(din3+dout3)O(d_{\text{in}}^3 + d_{\text{out}}^3), achieving two to three times the sample efficiency of TRPO and A2C without iterative conjugate gradient loops.

Think of It Like This

Measuring a Building by Floor Plan and Ceiling Height

Imagine you manage an auditorium renovation and need to adjust the acoustic damping of every seat. The auditorium has din=100d_{\text{in}} = 100 rows and dout=100d_{\text{out}} = 100 columns, creating 10,000 seat adjustments (W∈R100×100W \in \mathbb{R}^{100 \times 100}).

A naive acoustician attempts to measure the pairwise acoustic interaction between every individual seat and every other seat across the room. That requires recording a 10,000×10,00010{,}000 \times 10{,}000 matrix with 100,000,000 cross-correlations, taking days to calculate and calibrate.

A master acoustic engineer notices the geometry: sound waves disperse independently along the row axis (input sound propagation) and along the column axis (vertical ceiling reflection). Rather than measuring 100 million pairwise interactions, she measures a 100×100100 \times 100 row-covariance matrix AA and a 100×100100 \times 100 column-covariance matrix SS.

To compute the necessary damping correction for any seat, she inverts the 100×100100 \times 100 row matrix and the 100×100100 \times 100 column matrix independently, then sandwiches the target adjustment between them: S−1GA−1S^{-1} G A^{-1}. She turns an intractable 100-million-parameter inversion problem into two split calibrations that execute in seconds.

Where the analogy stops: In a real auditorium, physical sound interactions across rows and columns can have higher-order cross-dependencies (e.g., sound bouncing diagonally off corners). K-FAC explicitly treats activations and output gradients as statistically independent when forming the expectation E[(aa⊤)⊗(ss⊤)]≈E[aa⊤]⊗E[ss⊤]\mathbb{E}[ (a a^\top) \otimes (s s^\top) ] \approx \mathbb{E}[a a^\top] \otimes \mathbb{E}[s s^\top], accepting a slight structural approximation in exchange for massive computational tractability.

How It Actually Works

Natural Gradients and Kronecker-Factored Approximate Curvature

Consider a fully-connected layer in the actor or critic network mapping input activation a∈Rdina \in \mathbb{R}^{d_{\text{in}}} to pre-activation outputs s∈Rdouts \in \mathbb{R}^{d_{\text{out}}}:

s=Was = W a

Let LL denote the reinforcement learning surrogate objective, and define the pre-activation gradient gs=∇sLg_s = \nabla_s L. The gradient with respect to the weight matrix WW is an outer product:

∇WL=gsa⊤\nabla_W L = g_s a^\top

Vectorizing the weight gradient gives:

vec(∇WL)=a⊗gs\text{vec}(\nabla_W L) = a \otimes g_s

The Fisher Information Matrix block corresponding to layer weights WW is the expectation of the outer product of vectorized gradients:

Fl=E[vec(∇WL)vec(∇WL)⊤]=E[(aa⊤)⊗(gsgs⊤)]F_l = \mathbb{E}\left[ \text{vec}(\nabla_W L) \text{vec}(\nabla_W L)^\top \right] = \mathbb{E}\left[ (a a^\top) \otimes (g_s g_s^\top) \right]

Computing the expectation of this Kronecker product directly couples activations and backward gradients. The K-FAC approximation assumes statistical independence between the activation covariance and the gradient covariance:

Fl≈E[aa⊤]⊗E[gsgs⊤]=A⊗SF_l \approx \mathbb{E}[a a^\top] \otimes \mathbb{E}[g_s g_s^\top] = A \otimes S

where:

  • A=E[aa⊤]∈Rdin×dinA = \mathbb{E}[a a^\top] \in \mathbb{R}^{d_{\text{in}} \times d_{\text{in}}} is the uncentered second moment of activations.
  • S=E[gsgs⊤]∈Rdout×doutS = \mathbb{E}[g_s g_s^\top] \in \mathbb{R}^{d_{\text{out}} \times d_{\text{out}}} is the uncentered second moment of pre-activation derivatives.

The crucial mathematical property that makes K-FAC scalable is that the inverse of a Kronecker product is the Kronecker product of the individual inverses:

Fl−1≈(A⊗S)−1=A−1⊗S−1F_l^{-1} \approx (A \otimes S)^{-1} = A^{-1} \otimes S^{-1}

To obtain the vectorized natural gradient vec(∇~WL)=(A−1⊗S−1)vec(∇WL)\text{vec}(\tilde{\nabla}_W L) = (A^{-1} \otimes S^{-1}) \text{vec}(\nabla_W L), we leverage the matrix-vector identity (B⊗C)vec(X)=vec(CXB⊤)(B \otimes C) \text{vec}(X) = \text{vec}(C X B^\top). Because AA is symmetric (A−⊤=A−1A^{-\top} = A^{-1}), the natural gradient in weight matrix form simplifies to:

∇~WL=S−1(∇WL)A−1\tilde{\nabla}_W L = S^{-1} (\nabla_W L) A^{-1}

Instead of inverting a (dindout)×(dindout)(d_{\text{in}} d_{\text{out}}) \times (d_{\text{in}} d_{\text{out}}) matrix, ACKTR inverts one din×dind_{\text{in}} \times d_{\text{in}} matrix and one dout×doutd_{\text{out}} \times d_{\text{out}} matrix, followed by two fast matrix multiplications.

Trust Region Step and Covariance Running Averages

  1. Running Statistics: Across parallel environment workers, empirical covariances are tracked with an exponential moving average (momentum parameter α≈0.95\alpha \approx 0.95):

    At=αAt−1+(1−α)1B∑i=1Baiai⊤A_t = \alpha A_{t-1} + (1 - \alpha) \frac{1}{B} \sum_{i=1}^B a_i a_i^\top St=αSt−1+(1−α)1B∑i=1Bgs,igs,i⊤S_t = \alpha S_{t-1} + (1 - \alpha) \frac{1}{B} \sum_{i=1}^B g_{s, i} g_{s, i}^\top
  2. Damped Inverses: To guarantee invertibility and numerical stability, Tikhonov damping γ>0\gamma > 0 is added:

    (At+γI)−1,(St+γI)−1(A_t + \gamma I)^{-1}, \quad (S_t + \gamma I)^{-1}
  3. Trust Region Constraint: Let g~\tilde{g} be the concatenation of all natural gradients across layers, and gg be the concatenated standard gradients. The KL divergence constraint per update is enforced by scaling the natural gradient step:

    β=min⁡(1,2δg~⊤g)\beta = \min\left(1, \sqrt{\frac{2\delta}{\tilde{g}^\top g}}\right) Δθ=βg~\Delta \theta = \beta \tilde{g}

    where δ\delta is the maximum allowed average KL divergence (typically δ≈0.01\delta \approx 0.01).

Worked numerical example

Let us trace a concrete 2×22 \times 2 layer (din=2,dout=2d_{\text{in}} = 2, d_{\text{out}} = 2) step-by-step.

Suppose the accumulated activation covariance AA and pre-activation gradient covariance SS (including damping) are diagonal matrices:

A=[2.00.00.01.0],S=[4.00.00.02.0]A = \begin{bmatrix} 2.0 & 0.0 \\ 0.0 & 1.0 \end{bmatrix}, \quad S = \begin{bmatrix} 4.0 & 0.0 \\ 0.0 & 2.0 \end{bmatrix}

The first-order gradient matrix ∇WL=G\nabla_W L = G is:

G=[1.01.01.01.0]G = \begin{bmatrix} 1.0 & 1.0 \\ 1.0 & 1.0 \end{bmatrix}
  1. Invert Covariances:

    A−1=[0.50.00.01.0],S−1=[0.250.00.00.5]A^{-1} = \begin{bmatrix} 0.5 & 0.0 \\ 0.0 & 1.0 \end{bmatrix}, \quad S^{-1} = \begin{bmatrix} 0.25 & 0.0 \\ 0.0 & 0.5 \end{bmatrix}
  2. Compute Matrix-Form Natural Gradient:

    S−1G=[0.250.00.00.5][1.01.01.01.0]=[0.250.250.500.50]S^{-1} G = \begin{bmatrix} 0.25 & 0.0 \\ 0.0 & 0.5 \end{bmatrix} \begin{bmatrix} 1.0 & 1.0 \\ 1.0 & 1.0 \end{bmatrix} = \begin{bmatrix} 0.25 & 0.25 \\ 0.50 & 0.50 \end{bmatrix} G~=(S−1G)A−1=[0.250.250.500.50][0.50.00.01.0]=[0.1250.250.2500.50]\tilde{G} = (S^{-1} G) A^{-1} = \begin{bmatrix} 0.25 & 0.25 \\ 0.50 & 0.50 \end{bmatrix} \begin{bmatrix} 0.5 & 0.0 \\ 0.0 & 1.0 \end{bmatrix} = \begin{bmatrix} 0.125 & 0.25 \\ 0.250 & 0.50 \end{bmatrix}
  3. Verify Equivalence with Full Kronecker Inverse: The full 4×44 \times 4 Kronecker product F=A⊗SF = A \otimes S is:

    A⊗S=[2.0⋅S001.0⋅S]=diag(8.0,4.0,4.0,2.0)A \otimes S = \begin{bmatrix} 2.0 \cdot S & 0 \\ 0 & 1.0 \cdot S \end{bmatrix} = \text{diag}(8.0, 4.0, 4.0, 2.0)

    Its inverse is:

    (A⊗S)−1=diag(0.125,0.25,0.25,0.50)(A \otimes S)^{-1} = \text{diag}(0.125, 0.25, 0.25, 0.50)

    The column-major vectorized gradient is vec(G)=[1.0,1.0,1.0,1.0]⊤\text{vec}(G) = [1.0, 1.0, 1.0, 1.0]^\top. Applying the inverse:

    vec(G~)=(A⊗S)−1vec(G)=[0.125,0.25,0.25,0.50]⊤\text{vec}(\tilde{G}) = (A \otimes S)^{-1} \text{vec}(G) = [0.125, 0.25, 0.25, 0.50]^\top

    Reshaping back to a 2×22 \times 2 matrix yields exactly [0.1250.250.250.50]\begin{bmatrix} 0.125 & 0.25 \\ 0.25 & 0.50 \end{bmatrix}.

  4. Apply Trust-Region Scaling: Let the KL constraint limit be δ=0.01\delta = 0.01. The inner product between the natural gradient and the original gradient is:

    g~⊤g=(0.125)(1.0)+(0.25)(1.0)+(0.25)(1.0)+(0.50)(1.0)=1.125\tilde{g}^\top g = (0.125)(1.0) + (0.25)(1.0) + (0.25)(1.0) + (0.50)(1.0) = 1.125

    The scaling coefficient β\beta is:

    β=min⁡(1,2⋅0.011.125)=min⁡(1,0.017778)≈min⁡(1,0.13333)=0.13333\beta = \min\left(1, \sqrt{\frac{2 \cdot 0.01}{1.125}}\right) = \min\left(1, \sqrt{0.017778}\right) \approx \min(1, 0.13333) = 0.13333

    The resulting parameter step ΔW=βG~\Delta W = \beta \tilde{G} safely limits policy divergence.

Code

import numpy as np

class KFACLinearOptimizer:    """Computes Kronecker-factored natural gradients and trust-region updates
    for a linear feedforward layer W in R^{d_out x d_in}.    """
    def __init__(        self,        d_in: int,        d_out: int,        damping: float = 1e-3,        kl_clip: float = 0.01,        alpha: float = 0.95,    ) -> None:        self.d_in = d_in        self.d_out = d_out        self.damping = damping        self.kl_clip = kl_clip        self.alpha = alpha
        # Covariance matrices initialized to identity        self.cov_a = np.eye(d_in, dtype=np.float64)        self.cov_s = np.eye(d_out, dtype=np.float64)
    def update_covariances(        self,        activations: np.ndarray,        pre_grads: np.ndarray,    ) -> None:        """Updates running exponential moving averages of activation and gradient covariances.
        activations: shape (batch_size, d_in)        pre_grads: shape (batch_size, d_out)        """        batch_size = activations.shape[0]
        # Empirical batch covariances: E[a a^T] and E[s s^T]        batch_cov_a = (activations.T @ activations) / batch_size        batch_cov_s = (pre_grads.T @ pre_grads) / batch_size
        self.cov_a = self.alpha * self.cov_a + (1.0 - self.alpha) * batch_cov_a        self.cov_s = self.alpha * self.cov_s + (1.0 - self.alpha) * batch_cov_s
    def compute_natural_gradient(        self,        grad_w: np.ndarray,    ) -> tuple[np.ndarray, np.ndarray]:        """Computes natural gradient using fast matrix sandwich product S^{-1} G A^{-1}
        and compares it against the full Kronecker product inverse.        """        # Add Tikhonov damping        a_damped = self.cov_a + self.damping * np.eye(self.d_in)        s_damped = self.cov_s + self.damping * np.eye(self.d_out)
        # Invert d_in x d_in and d_out x d_out matrices        inv_a = np.linalg.inv(a_damped)        inv_s = np.linalg.inv(s_damped)
        # Fast matrix form: G_tilde = S^{-1} @ grad_w @ A^{-1}        nat_grad_matrix = inv_s @ grad_w @ inv_a
        # Full Kronecker inverse verification        kron_fim = np.kron(a_damped, s_damped)        inv_kron_fim = np.linalg.inv(kron_fim)        vec_grad = grad_w.flatten(order="F")        vec_nat_grad = inv_kron_fim @ vec_grad        nat_grad_kron = vec_nat_grad.reshape((self.d_out, self.d_in), order="F")
        return nat_grad_matrix, nat_grad_kron
    def step(self, grad_w: np.ndarray) -> tuple[np.ndarray, float]:        """Calculates trust-region scaled natural gradient step."""        nat_grad, _ = self.compute_natural_gradient(grad_w)
        # Inner product g^T * g_tilde across weights        g_dot_gtilde = float(np.sum(grad_w * nat_grad))
        # Trust region factor beta = min(1, sqrt(2 * delta / (g^T g_tilde)))        if g_dot_gtilde > 1e-12:            scale = min(1.0, np.sqrt(2.0 * self.kl_clip / g_dot_gtilde))        else:            scale = 1.0
        step_direction = scale * nat_grad        return step_direction, scale

if __name__ == "__main__":    np.set_printoptions(precision=4, suppress=True)
    # Initialize 2x2 layer optimizer    opt = KFACLinearOptimizer(d_in=2, d_out=2, damping=0.0, kl_clip=0.01)
    # Manually assign covariance matrices matching the worked numerical example    opt.cov_a = np.diag([2.0, 1.0])    opt.cov_s = np.diag([4.0, 2.0])
    grad_w = np.ones((2, 2), dtype=np.float64)
    nat_grad_mat, nat_grad_kron = opt.compute_natural_gradient(grad_w)    step_delta, scale_factor = opt.step(grad_w)
    print("Matrix Natural Gradient:")    print(nat_grad_mat)    # -> Matrix Natural Gradient:    # -> [[0.125 0.25 ]    # ->  [0.25  0.5  ]]
    print("\nVectorized Kronecker Gradient:")    print(nat_grad_kron)    # -> Vectorized Kronecker Gradient:    # -> [[0.125 0.25 ]    # ->  [0.25  0.5  ]]
    print(f"\nTrust Region Scaling Factor: {scale_factor:.6f}")    # -> Trust Region Scaling Factor: 0.133333
    print("\nStep Direction:")    print(step_delta)    # -> Step Direction:    # -> [[0.0167 0.0333]    # ->  [0.0333 0.0667]]
    # Ensure matrix form matches Kronecker form    assert np.allclose(nat_grad_mat, nat_grad_kron)    print("\nVerification Passed: Matrix form matches exact Kronecker inverse.")    # -> Verification Passed: Matrix form matches exact Kronecker inverse.

Watch Out For

Inversion Frequency Overhead and Damping Mismatch

Inverting AA and SS at every micro-step degrades computational throughput. In practice, ACKTR decouples the covariance update from matrix inversion: covariance running averages are accumulated on every mini-batch, but the costly matrix inversions (A+γI)−1(A + \gamma I)^{-1} and (S+γI)−1(S + \gamma I)^{-1} are recomputed only once every Tinv≈20T_{\text{inv}} \approx 20 to 100 steps.

Furthermore, if the Tikhonov damping parameter γ\gamma is chosen too small, empirical covariances can become near-singular, generating exploding gradient steps. Conversely, if γ\gamma is excessively large, (A+γI)−1≈γ−1I(A + \gamma I)^{-1} \approx \gamma^{-1} I, collapsing the natural gradient back into standard vanilla gradient descent and sacrificing all second-order curvature benefits. Keep γ\gamma calibrated to the variance of the policy distribution.

The Quick Version

  • Kronecker Factorization: ACKTR replaces the intractable D×DD \times D Fisher Information Matrix with layer-wise Kronecker products of input activations and output gradient covariances (Fl≈A⊗SF_l \approx A \otimes S).
  • Inversion Scalability: Leveraging (A⊗S)−1=A−1⊗S−1(A \otimes S)^{-1} = A^{-1} \otimes S^{-1} reduces inversion complexity from O((dindout)3)O((d_{\text{in}} d_{\text{out}})^3) to O(din3+dout3)O(d_{\text{in}}^3 + d_{\text{out}}^3), allowing natural gradients to run on modern GPUs.
  • Actor and Critic Optimization: Unlike TRPO, which applies second-order updates only to the actor, ACKTR applies K-FAC natural gradient updates to both policy and value networks.
  • No Conjugate Gradients: Eliminates the iterative Hessian-vector multiplication loops required by TRPO, delivering higher sample efficiency with consistent wall-clock throughput.