Backpropagation is the Adjoint Method

Backpropagation is often introduced as something developed within the field of machine learning. However, the reality is that backpropagation is just a special case of the adjoint method where the inversion of the partial derivative ๐œ•๐‘“๐œ•๐‘ข can be done efficiently due to its lower-triangular structure. This note is in two parts. In the first part we review the adjoint method while in the second part we describe how backpropagation is a special case of the adjoint method with a structure that results in scalable computational algorithms.

A key insight is that while the adjoint method is seemingly limited to equality constrained problems, it turns out that all computer programs are trivially equality constrained problems with equalities coming from each assignment11 All lines of code are essentially โ€œvariable=somethingโ€ which can be written as โ€œvariableโˆ’something=0โ€. In this sense, as shown in a later section, optimizing a neural network is actually an equality constrained optimization problem with constraints coming from each layer of the network. This is opposed to how optimizing a neural network is usually presented as an unconstrained optimization problem.

1. The Adjoint Method

In many engineering applications we are interested in solving equality constrained optimization problems where the optimization variable (๐œƒ) implicitly defines another variable (๐‘ข) that then appears in the objective function22 A famous example of this is Topology Optimization. Written out this means solving optimization problems of the following form

min๐œƒโˆˆโ„๐‘›๐œƒ๐ฟ(๐‘ข,๐œƒ),(some objective function)subject to ๐‘“(๐‘ข,๐œƒ)=0,(some equality constraint)๐‘ขโˆˆโ„๐‘›๐‘ข.

(1)

Most methods for solving equality constrained optimization problems require the computation of the gradient of the objective function with respect to the optimization variable ๐œƒ. This is where the problem comes in: Naively computing the gradient requires the computation of the sensitivities of the implicitly defined variable ๐‘ข with respect to the optimization variable ๐œƒ (i.e.d๐‘ขd๐œƒโˆˆโ„๐‘›๐‘ขร—๐‘›๐œƒ). This results in a computational bottleneck as forming the sensitivities scales as ๐’ช๏ธ€(๐‘›๐‘ข๐‘›๐œƒ), meaning that adding a new parameter adds ๐‘›๐‘ข additional sensitivities (and similarly adding one more variable would add ๐‘›๐œƒ additional sensitivities).

To resolve this computational bottleneck the adjoint method was introduced. The first step in the derivation of the adjoint method is to introduce the Lagrangian of the objective function, i.e.

โ„’๏ธ€(๐‘ข,๐œƒ,๐œ†)=๐ฟ(๐‘ข,๐œƒ)+๐œ†โŠค๐‘“(๐‘ข,๐œƒ).

(2)

It is easy to see that whenever ๐‘ข satisfies the equality constraint ๐‘“(๐‘ข,๐œƒ)=0, then the Lagrangian is equal to the original objective function. The reason why the introduction of the additional term in the Lagrangian is useful is that ๐œ† can be set to be anything. In particular, we aim to set it in a way so that we can avoid computing the otherwise computationally expensive sensitivities d๐‘ขd๐œƒ. We start by computing the total derivative of the Lagrangian with respect to ๐œƒ 33 The total derivative is the transpose of the gradient i.e. (d๐ฟd๐œƒ)โŠค=โˆ‡๐œƒ๐ฟ. That is multiplying the change in ๐œƒ with the total derivative (rather than the transpose of the gradient) gives the change in the function ๐ฟ.

dโ„’๏ธ€d๐œƒ=๐œ•๐ฟ๐œ•๐œƒ+๐œ•๐ฟ๐œ•๐‘ขd๐‘ขd๐œƒ+๐œ†โŠค(๐œ•๐‘“๐œ•๐œƒ+๐œ•๐‘“๐œ•๐‘ขd๐‘ขd๐œƒ).

(3)

Now we collect the terms that depend on the undesired d๐‘ขd๐œƒ, resulting in

dโ„’๏ธ€d๐œƒ=๐œ•๐ฟ๐œ•๐œƒ+๐œ†โŠค๐œ•๐‘“๐œ•๐œƒ+(๐œ•๐ฟ๐œ•๐‘ข+๐œ†โŠค๐œ•๐‘“๐œ•๐‘ข)d๐‘ขd๐œƒ.

(4)

As we can choose ๐œ† freely a natural idea is to set it in a way such that the term in front of the undesired term vanishes. This means that we can choose ๐œ† as the solution to the equation

๐œ•๐ฟ๐œ•๐‘ข+๐œ†โŠค๐œ•๐‘“๐œ•๐‘ข=0โ‡’๐œ†โŠค=โˆ’๐œ•๐ฟ๐œ•๐‘ข(๐œ•๐‘“๐œ•๐‘ข)โˆ’1.

(5)

Inserting ๐œ†โŠค back into the equation we find that the gradient of the Lagrangian with respect to ๐œƒ is given by

dโ„’๏ธ€d๐œƒ=๐œ•๐ฟ๐œ•๐œƒโˆ’๐œ•๐ฟ๐œ•๐‘ข(๐œ•๐‘“๐œ•๐‘ข)โˆ’1โŸ๐œ†โŠค๐œ•๐‘“๐œ•๐œƒ.

(6)

To conclude: The adjoint method is a simple way to avoid the computational bottleneck of computing the sensitivities d๐‘ขd๐œƒ by cleverly computing the so-called adjoint variable ๐œ† in a way that eliminates the need to compute the problematic sensitivities.

Exampleย 1: Linearly constrained problem with structure

Example 1 (Linearly constrained problem with structure)ย ย  In order to illustrate the adjoint method we consider a simple linearly constrained problem of the form (inspiration from problem 38 in [1] )

๐ฟ(๐‘ข,๐œƒ)=(๐‘โŠค๐‘ข(๐œƒ))2,๐‘“(๐‘ข,๐œƒ)=๐ด(๐œƒ)๐‘ขโˆ’๐‘=0,

(7)

where ๐ด(๐œƒ)โˆˆโ„๐‘›ร—๐‘› is a symmetric tridiagonal matrix that depends on the parameters ๐œƒโˆˆโ„2๐‘›โˆ’1 as

๐ด=[๐œƒ1๐œƒ๐‘›+10๐œƒ๐‘›+1๐œƒ2โ‹ฑโ‹ฑโ‹ฑโ‹ฑโ‹ฑ๐œƒ๐‘›โˆ’1๐œƒ2๐‘›โˆ’10๐œƒ2๐‘›โˆ’1๐œƒ๐‘›].

(8)

Now in order to compute the gradient of interest we start by computing the adjoint variable ๐œ† as

๐œ†โŠค=โˆ’๐œ•๐ฟ๐œ•๐‘ข(๐œ•๐‘“๐œ•๐‘ข)โˆ’1=โˆ’2(๐‘โŠค๐‘ข)๐‘โŠค๐ด(๐œƒ)โˆ’1.

(9)

Note that in practice we do not form ๐ด(๐œƒ)โˆ’1 explicitly but rather compute ๐œ† by solving the linear system ๐ด(๐œƒ)โŠค๐œ†=โˆ’2๐‘(๐‘โŠค๐‘ข). Using the adjoint variable we can compute the gradient of ๐ฟ with respect to ๐œƒ as

d๐ฟd๐œƒ=๐œ•๐ฟ๐œ•๐œƒโŸ=0+๐œ†โŠค๐œ•๐‘“๐œ•๐œƒ=๐œ†โŠค๐œ•๐ด๐œ•๐œƒ๐‘ข.

(10)

In the concrete case of the derivatives of ๐ด with respect to ๐œƒ๐‘– we have that

๐œ•๐ด๐œ•๐œƒ๐‘–=[๐›ฟ๐‘–,1๐›ฟ๐‘–,๐‘›+10๐›ฟ๐‘–,๐‘›+1๐›ฟ๐‘–,2โ‹ฑโ‹ฑโ‹ฑโ‹ฑโ‹ฑ๐›ฟ๐‘–,๐‘›โˆ’1๐›ฟ๐‘–,2๐‘›โˆ’10๐›ฟ๐‘–,2๐‘›โˆ’1๐›ฟ๐‘–,๐‘›].

(11)

Using this it follows that the ๐‘–th component of the gradient can be computed as

๐œ†โŠค๐œ•๐ด๐œ•๐œƒ๐‘–๐‘ข={๐œ†๐‘–๐‘ข๐‘–,๐‘–โ‰ค๐‘›,๐œ†๐‘–+1โˆ’๐‘›๐‘ข๐‘–โˆ’๐‘›+๐œ†๐‘–โˆ’๐‘›๐‘ข๐‘–+1โˆ’๐‘›,๐‘–>๐‘›.

(12)

Below is a Julia implementation of the above example along with a test against ForwardDiff.jl to verify the correctness of the implementation.

julia
1
using LinearAlgebra
2
using ForwardDiff
3
using Random
4
using Test
5
6
Random.seed!(42)
7
n = 1000 # Number of linear constraints
8
c = rand(n) # Objective vector
9
b = rand(n) # Linear equality vector
10
ฮธ = rand(2n-1) # Parameters of the equality constraint
11
12
# Parametrizing linear equality constraint matrix
13
A(ฮธ) = SymTridiagonal(ฮธ[1:n], ฮธ[n+1:end])
14
15
# Objective function
16
f(ฮธ) = (c' * (A(ฮธ) \ b))^2
17
18
# Partial derivatives
19
partial(i,n,ฮป,u) = i <= n ? ฮป[i]*u[i] : ฮป[i+1-n]*u[i-n] + ฮป[i-n]*u[i+1-n]
20
21
# Gradient computation
22
function โˆ‡f(ฮธ)
23
n = length(b)
24
M = A(ฮธ) # Defining constraint matrix
25
u = M \ b # Computing solution
26
ฮป = M \ (-2*c*(c'*u)) # Adjoint variables
27
return [partial(i,n,ฮป,u) for i = 1:2n-1]
28
end
29
30
# Testing against ForwardDiff
31
@test ForwardDiff.gradient(f,ฮธ) โ‰ˆ โˆ‡f(ฮธ)

2. Backpropagation and the Adjoint Method

Section 5.5 of [2] includes an example of how the adjoint method and backpropagation of neural networks are similar. In neural networks we are often interested in minimizing a loss function of the form

๐ฟ(๐œƒ;๐‘ข0)=โ€–ฮฆ๐‘(๐œƒ;๐‘ข0)โˆ’๐‘ฆโ€–22+๐œ‡โ„›๏ธ€(๐œƒ),

(13)

with ๐œƒโˆˆโ„๐‘˜,๐‘ข0โˆˆโ„๐‘š,๐‘ฆโˆˆโ„๐‘› and the notation โ€œ;๐‘ข0โ€ is used in order to highlight that ๐‘ข0 is a constant input (i.e. most often the โ€œdataโ€) and โ„›๏ธ€(๐œƒ) is some regularization function (e.g. โ„›๏ธ€(๐œƒ)=โ€–๐œƒโ€–22). Furthermore ฮฆ๐‘(๐œƒ;๐‘ข0):โ„๐‘˜โ†’โ„๐‘› is a neural network with ๐‘ layers that given a set of constant inputs ๐‘ข0 maps the parameters ๐œƒ to an output. Now, the above objective function does not include any equality constraints. However, one should realize that an ๐‘-layer neural network is nothing more than a composition of ๐‘ functions

ฮฆ๐‘(๐œƒ;๐‘ข0)=ฮฆ๐‘(ฮฆ๐‘โˆ’1(โ‹ฏ(ฮฆ1(๐œƒ;๐‘ข0)โ‹ฏ,๐œƒ;๐‘ข0),๐œƒ;๐‘ข0).

(14)

Now using the notation that ๐‘ข๐‘– is the output of the ๐‘–th layer of the network we can write the propagation through the network as a large nonlinear system of equations that have to be satisfied

๐‘“(๐‘ข,๐œƒ)=๐‘ขโˆ’ฮฆ(๐‘ข,๐œƒ)=[๐‘ข1๐‘ข2โ‹ฎ๐‘ข๐‘]โŸ๐‘ขโˆ’[ฮฆ1(๐œƒ;๐‘ข0)ฮฆ2(๐‘ข1,๐œƒ;๐‘ข0)โ‹ฎฮฆ๐‘(๐‘ข1,โ€ฆ,๐‘ข๐‘โˆ’1,๐œƒ;๐‘ข0)]โŸฮฆ(๐‘ข,๐œƒ)=[00โ‹ฎ0].

(15)

Using this notation (and removing the explicit dependence on ๐‘ข0) we see that optimizing a neural network is really nothing more than solving the following equality constrained optimization problem

min๐œƒ๐ฟ(๐‘ข,๐œƒ)=โ€–๐‘ข๐‘โˆ’๐‘ฆโ€–22+๐œ‡โ„›๏ธ€(๐œƒ),subject to ๐‘“(๐‘ข,๐œƒ)=0.

(16)

We can now use the adjoint method to compute the gradient of ๐ฟ with respect to ๐œƒ. As a reminder this means that

d๐ฟd๐œƒ=๐œ•๐ฟ๐œ•๐œƒโˆ’๐œ•๐ฟ๐œ•๐‘ข(๐œ•๐‘“๐œ•๐‘ข)โˆ’1โŸ๐œ†โŠค๐œ•๐‘“๐œ•๐œƒ,

(17)

where ๐œ† is referred to as the adjoint variable. Let ๐‘›๐‘– be the dimension of the output ๐‘ข๐‘– of layer ๐‘–, so that the stacked state ๐‘ข has dimension ๐‘‘=โˆ‘๐‘–=1๐‘๐‘›๐‘–. For simplicity, we assume that the final output ๐‘ข๐‘ is scalar, i.e. ๐‘›๐‘=1. Then ๐‘ข๐‘=๐‘’๐‘‘โŠค๐‘ข, where ๐‘’๐‘‘ is the last canonical basis vector in โ„๐‘‘. In this case we have that

๐œ•๐ฟ๐œ•๐‘ข=2(๐‘’๐‘‘โŠค๐‘ขโˆ’๐‘ฆ)๐‘’๐‘‘โŠค=2(๐‘ข๐‘โˆ’๐‘ฆ)๐‘’๐‘‘โŠค=๐‘”๐‘โŠค.

(18)

Now what is left to compute is ๐œ•๐‘“๐œ•๐‘ข and ๐œ•๐‘“๐œ•๐œƒ. Starting with ๐œ•๐‘“๐œ•๐‘ข we note that because ๐‘“ only propagates the ๐‘ข๐‘–โ€™s forwards the resulting partial derivative is a lower block triangular matrix of the form

๐œ•๐‘“๐œ•๐‘ข=[๐ผ0โ‹ฏ0โˆ’๐œ•ฮฆ2๐œ•๐‘ข1๐ผโ‹ฏ0โ‹ฎโ‹ฎโ‹ฑโ‹ฎโˆ’๐œ•ฮฆ๐‘๐œ•๐‘ข1โ‹ฏโˆ’๐œ•ฮฆ๐‘๐œ•๐‘ข๐‘โˆ’1๐ผ]=๐ฟ.

(19)

Now is a good place to stop and think of a key concept of backpropagation: The resulting computational graph must be a Directed Acyclic Graph (DAG). This is indeed equivalent to stating that ๐œ•๐‘“๐œ•๐‘ข should be a lower triangular matrix. This is an important property as the adjoint method requires us to invert ๐œ•๐‘“๐œ•๐‘ข, which can be done cheaply for a lower triangular matrix. Even more important is that the diagonal blocks are the identity, meaning that forward/backward substitutions can be done without any matrix inversions at all.

What is left is to compute ๐œ•๐‘“๐œ•๐œƒ, which standardly is done as

๐œ•๐‘“๐œ•๐œƒ=โˆ’[๐œ•ฮฆ1๐œ•๐œƒโ‹ฎ๐œ•ฮฆ๐‘๐œ•๐œƒ]โŸ๐‘€โŠค=โˆ’๐‘€โŠค.

(20)

Now, in case of the layers not sharing any parameters the matrix ๐‘€โŠค will have a block diagonal structure, i.e.

๐‘€โŠค=blkdiag(๐œ•ฮฆ1๐œ•๐œƒ1,โ€ฆ,๐œ•ฮฆ๐‘๐œ•๐œƒ๐‘),๐œƒ=[๐œƒ1โ‹ฎ๐œƒ๐‘],

(21)

where ๐œƒ๐‘– are the parameters of layer ๐‘–.

Putting everything together we find that the gradient of ๐ฟ with respect to ๐œƒ is given by

dโ„’๏ธ€d๐œƒ=๐œ•๐ฟ๐œ•๐œƒโˆ’๐œ•๐ฟ๐œ•๐‘ข(๐œ•๐‘“๐œ•๐‘ข)โˆ’1๐œ•๐‘“๐œ•๐œƒ=๐œ‡๐œ•โ„›๏ธ€๐œ•๐œƒโˆ’(๐‘”๐‘โŠค๐ฟโˆ’1(โˆ’๐‘€โŠค))=๐œ‡๐œ•โ„›๏ธ€๐œ•๐œƒ+2(๐‘ข๐‘โˆ’๐‘ฆ)๐‘’๐‘‘โŠค๐ฟโˆ’1๐‘€โŠค.

(22)

As noted previously both ๐ฟโˆ’1 and ๐‘€โŠค are structured, meaning that the above can be computed efficiently.

2.1. How to compute the elements of ๐ฟ and ๐‘€โŠค

For a standard linear layer an activation function ๐œŽ is usually applied element wise. In practice this means that we really should look at the rows of ฮฆ๐‘– separately. For the ๐‘—th row the gradients are easily seen to be

โˆ‡๐‘ข๐‘–โˆ’1๐œŽ(๐‘ค๐‘—โŠค๐‘ข๐‘–โˆ’1+๐‘๐‘—)=๐œŽโ€ฒ(๐‘ค๐‘—โŠค๐‘ข๐‘–โˆ’1+๐‘๐‘—)๐‘ค๐‘—,Goes into ๐ฟโˆ‡๐‘ค๐‘—๐œŽ(๐‘ค๐‘—โŠค๐‘ข๐‘–โˆ’1+๐‘๐‘—)=๐œŽโ€ฒ(๐‘ค๐‘—โŠค๐‘ข๐‘–โˆ’1+๐‘๐‘—)๐‘ข๐‘–โˆ’1,Goes into ๐‘€โŠคโˆ‡๐‘๐‘—๐œŽ(๐‘ค๐‘—โŠค๐‘ข๐‘–โˆ’1+๐‘๐‘—)=๐œŽโ€ฒ(๐‘ค๐‘—โŠค๐‘ข๐‘–โˆ’1+๐‘๐‘—),Goes into ๐‘€โŠค

(23)

Using the above we see that

๐œ•ฮฆ๐‘–๐œ•๐‘ข๐‘–โˆ’1=diag(๐œŽโ€ฒ(๐‘Š๐‘–๐‘ข๐‘–โˆ’1+๐‘๐‘–))๐‘Š๐‘–,

(24)

where ๐œŽโ€ฒ(โ‹…) is applied element wise. Now for ๐œ•ฮฆ๐‘–๐œ•๐œƒ๐‘– we have to pick a way of how to vectorize the parameters. Here we choose

๐œƒ๐‘–=[vec(๐‘Š๐‘–)๐‘๐‘–],

(25)

where vec(๐‘Š) vectorizes by stacking the columns of ๐‘Š. Using the vectorization we can write the sought after sensitivity as

๐œ•ฮฆ๐‘–๐œ•๐œƒ๐‘–=diag(๐œŽโ€ฒ(๐‘Š๐‘–๐‘ข๐‘–โˆ’1+๐‘๐‘–))([๐‘ข๐‘–โˆ’1โŠค1]โŠ—๐ผ๐‘›๐‘–).

(26)

While the Kronecker product above does result in a sparse matrix, it is even more structured. Using the standard property of Kronecker products, (๐ดโŠ—๐ต)๐‘ฃ=vec(๐ตmat(๐‘ฃ)๐ดโŠค), where mat(๐‘ฃ) reverses the column-stacking operation vec with dimensions compatible with ๐ด and ๐ต, its products with vectors can be computed without forming it, e.g. ([๐‘ข๐‘–โˆ’1โŠค1]โŠ—๐ผ๐‘›๐‘–)๐œƒ๐‘–=๐‘Š๐‘–๐‘ข๐‘–โˆ’1+๐‘๐‘–.

A final remark here is that the diagonal matrices that go into the elements of ๐ฟ and ๐‘€โŠค are the same. That is if we define

๐ท=diag(๐œŽโ€ฒ(๐‘Š1๐‘ข0+๐‘1),โ€ฆ,๐œŽโ€ฒ(๐‘Š๐‘๐‘ข๐‘โˆ’1+๐‘๐‘))=blkdiag(diag(๐œŽโ€ฒ(๐‘Š1๐‘ข0+๐‘1)),โ€ฆ,diag(๐œŽโ€ฒ(๐‘Š๐‘๐‘ข๐‘โˆ’1+๐‘๐‘))),

(27)

then it follows that

๐ฟ=๐ผโˆ’๐ทblkdiag(๐‘Š2,โ€ฆ,๐‘Š๐‘,โˆ’1),๐‘€โŠค=๐ทblkdiag([๐‘ข0โŠค1]โŠ—๐ผ๐‘›1,โ€ฆ,[๐‘ข๐‘โˆ’1โŠค1]โŠ—๐ผ๐‘›๐‘),

(28)

with the slightly unusual notation that blkdiag(โ‹…,โˆ’1) denotes the lower block diagonal matrix. Here its (๐‘–,๐‘–โˆ’1) block is ๐‘Š๐‘– for ๐‘–=2,โ€ฆ,๐‘, with all other blocks zero and block row and column sizes ๐‘›1,โ€ฆ,๐‘›๐‘. The weight ๐‘Š1 acts on the fixed input ๐‘ข0 and therefore does not appear in ๐ฟ.

Below is a Julia implementation of the above backpropagation/adjoint method for a fully connected feedforward neural network with scalar output. In this example we set ๐œ‡=0, so the objective contains only the squared error. The implementation uses BlockDiagonalMatrices.jl and Kronecker.jl in order to efficiently represent the involved matrices.

julia
1
using BlockDiagonalMatrices
2
using LinearAlgebra
3
using Kronecker
4
using Random
5
using Test
6
7
Random.seed!(42)
8
9
h(x) = exp(-x) # activation function
10
โˆ‡h(x) = -exp(-x) # derivative of activation function
11
12
# Forward pass including the derivatives
13
function forward_pass(u0, layers)
14
u = u0
15
_, b1 = first(layers)
16
derivs = typeof(b1)[]
17
kron_blocks = typeof([u; 1]' โŠ— I(length(b1)))[]
18
for (W, b) in layers
19
push!(kron_blocks,[u; 1]' โŠ— I(length(b))) # Lazy Kronecker product
20
z = W*u + b
21
u = h.(z)
22
push!(derivs, โˆ‡h.(z))
23
end
24
return kron_blocks, derivs, u
25
end
26
# Block back substitution: solves L^T y = b, i.e. y = L^(-T) b
27
function backsub(derivs, weights, b)
28
y = Matrix(b)
29
stop = length(y)
30
@views for (d,Wt) in zip(reverse(derivs), reverse(transpose.(weights)))
31
nprev, n = size(Wt)
32
src = stop-n+1:stop
33
dst = stop-n-nprev+1:stop-n
34
tmp = d .* y[src]
35
# Equivalent to: y[dst] .+= Wt * (d .* y[src])
36
mul!(y[dst], Wt, tmp, 1, 1)
37
stop -= n
38
end
39
return y
40
end
41
# Helper function that unpacks a vector ฮธ into Ws and bs
42
function unpack_ฮธ(ฮธ, layer_sizes)
43
Ws, bs = Matrix{eltype(ฮธ)}[], Vector{eltype(ฮธ)}[]
44
i = 1
45
for (n_in,n_out) in zip(layer_sizes[1:end-1], layer_sizes[2:end])
46
j = i + n_out*n_in
47
push!(Ws, reshape(ฮธ[i:j-1], n_out, n_in))
48
i = j + n_out
49
push!(bs, ฮธ[j:i-1])
50
end
51
return Ws, bs
52
end
53
# Evaluate the network output using the forward pass
54
function network_output(ฮธ, layer_sizes, u0)
55
Ws, bs = unpack_ฮธ(ฮธ, layer_sizes)
56
_, _, uN = forward_pass(u0, zip(Ws,bs))
57
return only(uN)
58
end
59
# Gradient computation using the adjoint method
60
function โˆ‡L(ฮธ, layer_sizes, u0; y=0.0)
61
# First unpack the parameter vector
62
Ws, bs = unpack_ฮธ(ฮธ, layer_sizes)
63
# Forward pass includes derivative information
64
kron_blocks, derivs, uN = forward_pass(u0, zip(Ws, bs))
65
# S has subdiagonal blocks W_2,...,W_N; L = I - D*S and M^T = D*K
66
D = Diagonal(vcat(derivs...))
67
K = BlockDiagonal(kron_blocks)
68
g = zeros(eltype(ฮธ), sum(layer_sizes[2:end]))
69
g[end] = 2*(only(uN) - y)
70
# The final step is to evaluate the gradient from the right
71
grad_adjoint = (backsub(derivs[2:end],Ws[2:end],g')*D)*K
72
return grad_adjoint'
73
end
74
# Forward difference to test the adjoint gradient implementation
75
function fd(ฮธ, layer_sizes, u0, i; e=1e-6, y=2.0)
76
uN0 = network_output(ฮธ, layer_sizes, u0)
77
ฮธ[i] += e
78
uN1 = network_output(ฮธ, layer_sizes, u0)
79
ฮธ[i] -= e
80
return ((uN1 - y)^2 - (uN0 - y)^2)/e
81
end
82
83
# Setting up parameters
84
85
layer_sizes = [50,40,30,20,10,1]
86
N = length(layer_sizes) - 1
87
init(sizes...) = 0.01*randn(sizes...)
88
Ws = [init(layer_sizes[i+1], layer_sizes[i]) for i=1:N]
89
bs = [init(layer_sizes[i+1]) for i = 1:N]
90
u0 = init(layer_sizes[1])
91
layers = zip(Ws, bs)
92
93
# First we compute the forward pass
94
ฮธvec = vcat([[W[:]; b] for (W, b) in layers]...)
95
96
## Testing the gradient
97
y = 3.0 # We aim to have the final output be 3.0
98
grad_adjoint = โˆ‡L(ฮธvec, layer_sizes, u0; y=y)
99
idx = 4000
100
grad_fd = fd(ฮธvec, layer_sizes, u0, idx; e=1e-5, y=y)
101
@test grad_fd โ‰ˆ grad_adjoint[idx] atol=1e-6
102
103
# Optimizing to get the desired output y
104
for _ = 1:1000
105
grad = โˆ‡L(ฮธvec, layer_sizes, u0; y=y)
106
ฮธvec .-= 0.001 .* grad
107
end
108
@test network_output(ฮธvec, layer_sizes, u0) โ‰ˆ y
109
final_gradient = โˆ‡L(ฮธvec, layer_sizes, u0; y=y)
110
@test final_gradient โ‰ˆ zeros(length(ฮธvec)) atol=1e-10

Cite this post

bibtex
@misc{paltorp2025adjointmethod,
author = {Paltorp, Mikkel},
title = {{Backpropagation is the Adjoint Method}},
year = {2025},
month = oct,
url = {https://mipals.github.io/pdf/2025/10-25-adjoint-method/adjoint-method.pdf},
note = {Blog post}
}

Bibliography

  • [1] P. Bright, A. Edelman, and S. G. Johnson, โ€œMatrix Calculus (for Machine Learning and Beyond).โ€ [Online]. Available: https://arxiv.org/abs/2501.14787
  • [2] A. Edelman, E. Akyรผrek, and Y. Wang, โ€œBackpropagation through Back Substitution with a Backslash,โ€ SIAM Journal on Matrix Analysis and Applications, vol. 45, no. 1, pp. 429โ€“449, 2024, doi: 10.1137/22M1532871.