Double Derivatives
SymbolicNeuralNetworks can compute derivatives of arbitrary order, by feeding the symbolic result of one derivative into the next. This is the case Zygote-based AD struggles with, and the reason this package exists.
The two building blocks are Jacobian (differentiate with respect to the input) and Gradient (differentiate with respect to the parameters); see Derivatives for each on its own.
Jacobian of a neural network
using AbstractNeuralNetworks
using SymbolicNeuralNetworks
using SymbolicNeuralNetworks: Jacobian, Gradient, derivative
using Latexify: latexify
c = Chain(Dense(2, 1, tanh; use_bias = false))
nn = SymbolicNeuralNetwork(c)
□ = Jacobian(nn)
derivative(□) |> latexify\begin{equation}
\left[
\begin{array}{cc}
\mathtt{W\_1_{1}ˏ_1} ~ \left( 1 - \tanh^{2}\left( \mathtt{W\_1_{1}ˏ_1} ~ \mathtt{x_1} + \mathtt{W\_1_{1}ˏ_2} ~ \mathtt{x_2} \right) \right) & \mathtt{W\_1_{1}ˏ_2} ~ \left( 1 - \tanh^{2}\left( \mathtt{W\_1_{1}ˏ_1} ~ \mathtt{x_1} + \mathtt{W\_1_{1}ˏ_2} ~ \mathtt{x_2} \right) \right) \\
\end{array}
\right]
\end{equation}
The output of nn is one-dimensional and the convention is
\[\square_{ij} = [\mathrm{jacobian}_{x}f]_{ij} = \frac{\partial}{\partial{}x_j}f_i,\]
so the result has shape $\mathrm{output\_dim}\times\mathrm{input\_dim} = 1\times2$:
size(derivative(□))(1, 2)Gradient of a neural network
Gradient differentiates every element of an array-valued expression with respect to the network parameters:
g = Gradient(nn)
derivative(g)[1].L1.W |> latexify\begin{equation}
\left[
\begin{array}{cc}
\left( 1 - \tanh^{2}\left( \mathtt{W\_1_{1}ˏ_1} ~ \mathtt{x_1} + \mathtt{W\_1_{1}ˏ_2} ~ \mathtt{x_2} \right) \right) ~ \mathtt{x_1} & \left( 1 - \tanh^{2}\left( \mathtt{W\_1_{1}ˏ_1} ~ \mathtt{x_1} + \mathtt{W\_1_{1}ˏ_2} ~ \mathtt{x_2} \right) \right) ~ \mathtt{x_2} \\
\end{array}
\right]
\end{equation}
Combining the two
Feeding the Jacobian into a Gradient differentiates the network twice — first with respect to its input, then with respect to its parameters:
g = Gradient(derivative(□), nn)The result is a matrix (of the shape of the Jacobian) of parameter sets. To read the derivative of the first Jacobian entry with respect to the weight W of the first layer:
matrix_index = (1, 1)
layer = :L1
weight = :W
derivative(g)[matrix_index...][layer][weight] |> latexify\begin{equation}
\left[
\begin{array}{cc}
1 - \tanh^{2}\left( \mathtt{W\_1_{1}ˏ_1} ~ \mathtt{x_1} + \mathtt{W\_1_{1}ˏ_2} ~ \mathtt{x_2} \right) - 2 ~ \mathtt{W\_1_{1}ˏ_1} ~ \tanh\left( \mathtt{W\_1_{1}ˏ_1} ~ \mathtt{x_1} + \mathtt{W\_1_{1}ˏ_2} ~ \mathtt{x_2} \right) ~ \left( 1 - \tanh^{2}\left( \mathtt{W\_1_{1}ˏ_1} ~ \mathtt{x_1} + \mathtt{W\_1_{1}ˏ_2} ~ \mathtt{x_2} \right) \right) ~ \mathtt{x_1} & - 2 ~ \mathtt{W\_1_{1}ˏ_1} ~ \tanh\left( \mathtt{W\_1_{1}ˏ_1} ~ \mathtt{x_1} + \mathtt{W\_1_{1}ˏ_2} ~ \mathtt{x_2} \right) ~ \left( 1 - \tanh^{2}\left( \mathtt{W\_1_{1}ˏ_1} ~ \mathtt{x_1} + \mathtt{W\_1_{1}ˏ_2} ~ \mathtt{x_2} \right) \right) ~ \mathtt{x_2} \\
\end{array}
\right]
\end{equation}
build_nn_function turns the whole thing into an executable function. We evaluate it at
\[x = \begin{pmatrix} 1 \\ 0 \end{pmatrix}, \quad W = \begin{bmatrix} 1 & 0 \end{bmatrix}\]
using AbstractNeuralNetworks: params
using NeuralNetworkParameters: NetworkParameters
built_function = build_nn_function(derivative(g), params(nn), nn.input)
x = [1.0, 0.0]
ps = NetworkParameters((L1 = (W = [1.0 0.0],),))
built_function(x, ps)[matrix_index...][layer][weight]1×2 Matrix{Float64}:
-0.219726 -0.0With Jacobian, Gradient and build_nn_function, combinations of derivatives are just function composition on symbolic expressions. Every entry of the result above is generated as part of a single function, so the forward pass they share is computed once; see Equation Sets.