, and the derivatives come for free.
Forward mode, at a snail's pace: one direction at a time.
My shell has a real part and an part.
Teach the computer how sin treats . Teaching me takes longer.
Dual Numbers for Automatic Differentiation
1. Scalar derivatives
Dual numbers are a key part of forward-mode differentiation. However, I find the introduction of them unintuitive. Usually, as done in [1], they are introduced such that
(1)
where . This is not incorrect. However, I think it is more natural to think of dual numbers, at least in the context of automatic differentiation, as a number type that simply tracks both and at the same time by introducing an . This is sort of similar to how a complex number can be seen to track an and at the same time by introducing an . However, for complex numbers the aim is to be able to solve . For dual numbers the intent, as noted before, is to track and at the same time. We therefore define how a function acts on a dual number by the composition rule
(2)
of which (1) is the special case . In the same way we define multiplication and division of dual numbers by the product and quotient rules
(3)
and addition and subtraction by the sum and difference rules
(4)
Now an initial complaint would be: What if (or ) is not knownβwhich is usually the case when forward-mode differentiation is applied to a general function ? The answer is that we donβt need to know them as long as the derivatives of the basic operations that make up are known. That is, we can simply evaluate , which by (2) should give us the derivative of at as the coefficient of . In the code example below we define the following composition rules for the elementary functions and as
(5)
Note that in general does not have to come from the derivative of with respect to some variable.
ExampleΒ 1: Evaluating a function on a dual number
Example 1 (Evaluating a function on a dual number)Β Β Let and . Evaluating one operation at a time using the rules above gives
(6)
The coefficient of is , even though was never written down.
ExampleΒ 2: Scalar derivative in Julia
Example 2 (Scalar derivative in Julia)Β Β Julia lets us define the arithmetic of a new type directly, so the scalar case takes only a few lines of code.
1# A small, deliberately incomplete forward-mode AD implementation.2struct Dual{T<:Real}3 value::T4 deriv::T5end6# Promote mixed inputs such as Dual(2, 1.0) to a common type7Dual(value::Real, deriv::Real) = Dual(promote(value, deriv)...)89# Product rule: (f * g)' = f' * g + f * g'10Base.:*(f::Dual, g::Dual) = Dual(11 f.value * g.value,12 f.deriv * g.value + f.value * g.deriv,13)14Base.:*(f::Dual, g::Real) = f * Dual(g, zero(g))15Base.:*(f::Real, g::Dual) = Dual(f, zero(f)) * g1617# Quotient rule: (f / g)' = (f' * g - f * g') / g^218Base.:/(f::Dual, g::Dual) = Dual(19 f.value / g.value,20 (f.deriv * g.value - f.value * g.deriv) / g.value^2,21)2223# Composition rule: (f β g)' = (f' β g) * g'24Base.sin(f::Dual) = Dual(sin(f.value), cos(f.value) * f.deriv)25Base.exp(f::Dual) = Dual(exp(f.value), exp(f.value) * f.deriv)2627# Addition rules: (f + g)' = f' + g', (f + c)' = f', (c + f)' = f'28Base.:+(f::Dual, g::Dual) = Dual(f.value + g.value, f.deriv + g.deriv)29Base.:+(f::Dual, c::Real) = f + Dual(c, zero(c))30Base.:+(c::Real, g::Dual) = Dual(c, zero(c)) + g3132# Subtraction rules: (-f)' = -f', (f - g)' = f' - g'33Base.:-(f::Dual) = Dual(-f.value, -f.deriv)34Base.:-(f::Dual, g::Dual) = f + (-g)3536# The derivative is the epsilon coefficient of f(x + 1*epsilon)37derivative(f, x) = f(Dual(x, one(x))).deriv3839# Dual is not a Number, so Base's generic ^ does not apply to it and40# f_scalar(x) = sin(x^2) + x would throw a MethodError41f_scalar(x) = sin(x * x) + x42@assert isapprox(derivative(f_scalar, 2.0), 4cos(4.0) + 1)4344println("f'(2) = ", derivative(f_scalar, 2.0))2. Jacobian-vector products
For a multivariate function the simplest extension keeps a single and seeds each input with a direction . The dual numbers then track the function value and a directional derivative (a Jacobian-vector product) at the same time, i.e.,
(7)
where is the Jacobian. This definition follows directly by looking at the th entry as
(8)
A thing to note here is that the sum is computed automatically by the dual number arithmetic, and hence we never form the Jacobian. From a scaling perspective, this is a big win as the Jacobian is in size, and hence can be very large.
The price is that one evaluation gives one product . Its cost is a small multiple of the cost of evaluating , but the full Jacobian requires evaluations, one for each unit vector . Forward mode is therefore well suited when is small compared to . In the opposite case reverse mode (backpropagation) is preferred. It computes one vector-Jacobian product per evaluation, also at a small multiple of the cost of evaluating , so the full Jacobian requires evaluations, one for each unit vector . The price is that the intermediate values of must be stored for the backward pass. The common case of a scalar loss of many parameters () needs only a single evaluation; see Backpropagation as a Linear Solve.11 In the notation of that post, the Jacobian of a network is , where is block lower triangular. Forward mode multiplies from the right, , so the solve with is a forward substitution that follows the layers in order, exactly as the dual numbers do. Reverse mode multiplies from the left, , so the solve becomes the backward substitution .
ExampleΒ 3: Jacobian-vector product in Julia
Example 3 (Jacobian-vector product in Julia)Β Β Let
(9)
At in the direction , (7) gives
(10)
1@isdefined(Dual) || include("dual_numbers.jl")23function jvp(f, x, v)4 @assert length(x) == length(v)5 seeded = [Dual(xi, vi) for (xi, vi) in zip(x, v)]6 result = f(seeded)7 return map(z -> z.value, result), map(z -> z.deriv, result)8end910f_vector(x) = (x[1] * x[2] + sin(x[1]), exp(x[2]))11x, v = [2.0, 3.0], [1.0, -1.0]12value, deriv = jvp(f_vector, x, v)13@assert all(isapprox.(value, (6 + sin(2.0), exp(3.0))))14@assert all(isapprox.(deriv, (1 + cos(2.0), -exp(3.0))))1516println("f($x) = ", value)17println("Jf($x)$v = ", deriv)Cite this post
@misc{paltorp2026dualnumbers, author = {Paltorp, Mikkel}, title = {{Dual Numbers for Automatic Differentiation}}, year = {2026}, month = sep, url = {https://mipals.github.io/pdf/2026/09-25-dual-numbers/dual-numbers.pdf}, note = {Blog post}}Bibliography
- [1] J. Revels, M. Lubin, and T. Papamarkou, βForward-Mode Automatic Differentiation in Julia,β arXiv, 2016, [Online]. Available: https://arxiv.org/abs/1607.07892