Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

No12 - Programación Diferencial Pt2

Programación Diferenciable: Métodos Forward Pt2

Fecha: 01/06/2026

Estas notas prosiguen la temática de Programación diferenciable de la clase pasada, retomando la introducción a Diferenciación automática directa (Forward AD). De los métodos forward discretos vistos en la materia, este es el que se usa en la práctica, por su simplicidad y exactitud en el cómputo. En particular, comenzamos con una implementación de Forward AD en Julia, haciendo uso de los números duales.

Números duales

Se define a los números duales como una extensión de los reales, comenzando por definir a ϵ\epsilon número abstracto cumpliendo

ϵ2=0;ϵ0\epsilon^{2} = 0\quad ;\quad \epsilon \neq 0

y escribiendo a todo número dual como

xϵ=x1+ϵx2x_{\epsilon} = x_1 + \epsilon x_2

en donde a x1x_1 será el valor de xϵx_{\epsilon} y x2x_2 será su derivada.

En lo que a la AD respecta, los números duales son una manera muy fácil y directa de implementarla en un lenguaje de programación con herramientas de POO como lo es Julia.

@ksdef struct DualNumber{F <: AbstractFloat}
            value: F
            derivative: F
end

Queremos ser capaces de operar sobre los números duales, sumar, multiplicar, aplicar funciones elementales. Una de las cosas buenas de Julia es su simplicidad a la hora de extender operaciones a nuevas estructuras de datos.

#Define operations on dual numbers
function Base.:(+)(a::DualNumber, b::DualNumber)
    res_value = a.value + b.value
    res_derivative = a.derivative + b.derivative
    return DualNumber(res_value, res_derivative)
end

function Base.:(*)(a::DualNumber, b::DualNumber)
    res_value = a.value * b.value
    res_derivative = a.value * b.derivative + a.derivative * b.value 
    return DualNumber(res_value, res_derivative)
end

Esto nos va a permitir instanciar los números duales y operar sobre ellos, trasladando siempre en la parte dual la derivada correspondiente a la ejecución de las operaciones.

Si creamos 2 números duales:

a = DualNumber(1.0, 0.0)
b = DualNumber(2.0, 1.0)

Y hacemos la operación a + b deberia devolver:

a + b = DualNumber(3.0, 1.0)

Analogamente, si creamos 2 números duales:

a = DualNumber(0.9, 0.0)
b = DualNumber(1.4, 1.0)

Y hacemos la operación a * b deberia devolver:

a * b = DualNumber(1.4 * 0.9, 0.9)

De esta forma, se puede observar como la parte dual arrastra el valor de la derivada, y esto se puede hacer cuantas veces uno quiera.

Sigamos con uan función un poco más compleja.

#Define operations on dual numbers
function Base.:(sin(x))(a::DualNumber)
    res_value = sin(x)(a.value)
    res_derivative = cos(a.value) * a.derivative
    return DualNumber(res_value, res_derivative)
end

Como estas funciones, se pueden crear tantas como operaciones tengamos, sin(x) importar que sean unitarias, binarias, etc.

Siempre lo que uno consigue es que la primer componente tenga el valor y la segunda componente sea su derivada.

Los números duales son muy útiles a la hora de calcular derivadas parciales e incluso direccionales, debido a que esta estructura permite flexibilizar hacia donde esta derivando uno.

Ejemplo
Si hubiesemos querido derivar respecto a b, solo deberiamos haber modificado el input de la siguiente manera:

a = DualNumber(0.9, 1.0)
b = DualNumber(1.4, 0.0)
a * b = DualNumber(1.4 * 0.9, 1.4)

En la práctica uno no crea todas estas funciones desde 0, ya que existe una libreria que contiene todas estas funciones y muchas más. Uno solo la importa y usa todas las herramientas que provee esta librería.

Ejemplo

usin(x)g ForwardDiff

x = ForwardDiff.Dual(2.0, 1.0)

y = x^2 + 3x

A diferencia de lo que hacíamos en diferencias finitas el valor de la derivada usando Forward AD es exacto.

Si probamos esto con diferencias finitas:

La derivada se aproxima mediante

f(a+ϵ)f(a)ϵ\frac{f(a+\epsilon)-f(a)}{\epsilon}
f(x) = sin(x)(x * 0.9)

epsilon = 1e-10
@show (f(a.value + ϵ ) - f(a.value)) / ϵ  

Mientras que con Forward AD el cálculo de la derivada es exacto, con diferencias finitas comienzan a haber errores de truncación debido a la sensibilidad del resultado con respecto a ϵ\epsilon.

Una contra de este método es que viene con un costo de memoria más alto, debido a que ahora estamos trabajando no solo con su valor sin(x)o que también con su derivada.

En ecuaciones diferenciales, uno propaga el número dual en el solver númerico y consigue la solución y la derivada de esa solución con respecto a los parámetros.

Veamos una representación de lo que sucede con cada método:

Gráfico

Se observa que con la solución exacta de diferencias finitas, el error baja y luego vuelve a subir debido al error de truncado.

Además, la otra curva refleja la solución exacta de diferenciacion compleja, donde se ve que la misma baja hasta 10-16, el error de máquina.

Por último, la curva violeta representa el error de forward AD. En este caso, se puede observar que el mismo se adapta totalmente a la tolerancia ya que en ambos gráficos la curva se mantiene constante sobre la tolerancia en cada caso respectivamente.

En resumen, diferencias fínitas es el método menos exacto ya que contiene error de truncado, mientras que diferenciación compleja baja hasta error de máquina a partir de un cierto ϵ\epsilon. Forward AD no depende de ϵ\epsilon, por lo que, en caso de que la tolerancia fuese el error de máquina, la curva se mantendría constante en ese valor.

Comparación matemática: Diferenciación Compleja vs Forward AD

Para entender la diferencia fundamental entre ambos métodos forward, desarrollamos paso a paso la derivada de f(x)=sin(x2)f(x) = \sin(x^2) en un punto xx cualquiera.

El resultado esperado es: f(x)=2xcos(x2)f'(x) = 2x\cos(x^2)

Método 1: Diferenciación Compleja

Idea central: Evaluamos la función en un punto complejo x+iεx + i\varepsilon y extraemos la derivada de la parte imaginaria.

Paso 1 — Definimos la Variable compleja

Definimos:

x=x1+ix2x = x_1 + ix_2 con i2=1i^2 = -1

y para calcular la derivada, tomamos x2=εx_2 = \varepsilon muy pequeño:

x=x1+iεx = x_1 + i\varepsilon

Aca:

Paso 2 — Elevamos al cuadrado la variable compleja

(x1+iε)2=x12+2iεx1+(iε)2=x12+2iεx1ε2(x_1 + i\varepsilon)^2 = x_1^2 + 2i\varepsilon x_1 + (i\varepsilon)^2 = x_1^2 + 2i\varepsilon x_1 - \varepsilon^2

En particular,

Paso 3 — Aplicamos el seno

Para este caso, usamos la siguiente identidad

sin(a+bi)=sin(a)cosh(b)+icos(a)sinh(b)\sin(a + bi) = \sin(a)\cosh(b) + i\cos(a)\sinh(b)
sin(x2)=sin(x12ε2)cosh(2x1ε)+icos(x12ε2)sinh(2x1ε)\sin(x^2) = \sin(x_1^2 - \varepsilon^2)\cosh(2x_1\varepsilon) + i\cos(x_1^2 - \varepsilon^2)\sinh(2x_1\varepsilon)

Paso 4 — Extraemos la derivada

La fórmula de diferenciación compleja nos dice que la derivada está en la parte imaginaria, dividida por ε\varepsilon:

dfdx=limε0Im(f(x+iε))ε=limε0cos(x12ε2)sinh(2x1ε)ε\frac{df}{dx} = \lim_{\varepsilon \to 0} \frac{\text{Im}(f(x + i\varepsilon))}{\varepsilon} = \lim_{\varepsilon \to 0} \frac{\cos(x_1^2 - \varepsilon^2)\sinh(2x_1\varepsilon)}{\varepsilon}

Paso 5 — Tomamos el límite ε0\varepsilon \to 0

Como ε2=0\varepsilon^2 = 0

dfdx=limε0cos(x12ε2)sinh(2x1ε)ε=limε0cos(x12)sinh(2x1ε)ε\frac{df}{dx} = \lim_{\varepsilon \to 0} \frac{\cos(x_1^2 - \varepsilon^2) \cdot \sinh(2x_1\varepsilon)}{\varepsilon} = \lim_{\varepsilon \to 0} \frac{\cos(x_1^2) \cdot \sinh(2x_1\varepsilon)}\varepsilon

Luego, usando que sinh(z)z\sinh(z) \approx z para z0z \to 0 entonces sinh(2x1ε)=2x1ε\sinh(2x_1\varepsilon) = 2x_1\varepsilon:

dfdx=limε0cos(x12)2x1εε\frac{df}{dx} = \lim_{\varepsilon \to 0} \frac{\cos(x_1^2) \cdot 2x_1\varepsilon}{\varepsilon}

y dividiendo por ε\varepsilon:

dfdx=cos(x12)2x1\frac{df}{dx} = \cos(x_1^2) \cdot 2x_1

Método 2: Forward AD (Números Duales)

Idea central: Evaluamos la función en un “número dual” x+εx + \varepsilon y la derivada aparece directamente como coeficiente de ε\varepsilon.

Paso 1 — Definimos la Variable dual

Definimos xε=x1+εx_\varepsilon = x_1 + \varepsilon con ε2=0,ε0\varepsilon^2 = 0, \quad \varepsilon \neq 0

Aca:

Paso 2 — Elevamos al cuadrado

xε2=(x1+ε)2=x12+2x1ε+ε2=x12+2x1εx_\varepsilon^2 = (x_1 + \varepsilon)^2 = x_1^2 + 2x_1\varepsilon + \varepsilon^2 = x_1^2 + 2x_1\varepsilon

Obtenemos un número dual donde:

Paso 3 — Aplicamos Seno

Expandimos por Taylor:

sin(x12+2x1ε)=sin(x12)+cos(x12)(2x1ε)sin(x12)2(2x1ε)2+...\sin(x_1^2 + 2x_1\varepsilon) = \sin(x_1^2) + \cos(x_1^2) \cdot (2x_1\varepsilon) - \frac{\sin(x_1^2)}{2}(2x_1\varepsilon)^2 + ...

Pero (2x1ε)2=4x12ε2=0(2x_1\varepsilon)^2 = 4x_1^2\varepsilon^2 = 0, por lo que todos los términos de orden 2\geq 2 desaparecen:

sin(xε2)=sin(x12)+ε2x1cos(x12)\sin(x_\varepsilon^2) = \sin(x_1^2) + \varepsilon \cdot 2x_1\cos(x_1^2)

Entonces volviendo a la idea central, ya tenemos de forma explicita la derivada (tomamos la parte del coeficiente de ε\varepsilon):

dfdx=2x1cos(x12)\frac{df}{dx} = 2x_1\cos(x_1^2)

Conclusión: En el método de números duales (Forward AD) podemos obtener la derivada de forma inmediata (solamente una en una evaluación de la función tenemos el valor real y su derivada) sin necesidad de usar trucos matematicos, meternos con los limites o tener que extraer la parte imaginaria de un resultado. Es decir, si integramos esto dentro de un solver de ODEs, en cada paso tenemos la derivada correspondiente y de forma directa y automática.

Metodos Continuos Forward

Idea central: A diferencia de los métodos discretos, donde se toma un solver numérico ya discretizado y se diferencia su algoritmo paso a paso, en los métodos continuos la estrategia es diferenciar primero y luego discretizar. Esto permite que al tomar como punto de entrada la propia ecuación diferencial, el cálculo de las sensibilidades se vuelve independiente de la lógica interna del solver, evitando asi depender del error numérico de la discretización.

Ecuación de Sensibilidad

Tenemos una Ecuación Diferencial Ordinaria que depende de ciertos parámetros θ\theta:

dudt=f(u,t,θ),u(t0)=u0\frac{du}{dt} = f(u, t, \theta), \quad u(t_0) = u_0

y una función de pérdida a minimizar:

L(θ)=L(u(,θ),θ)L(\theta) = L(u(\cdot, \theta), \theta)

Para optimizar θ\theta, necesitamos el gradiente de la pérdida. Usando la regla de la cadena:

dLdθ=Luuθ+Lθ\frac{dL}{d\theta} = \frac{\partial L}{\partial u} \cdot \frac{\partial u}{\partial \theta} + \frac{\partial L}{\partial \theta}

Mini ejemplo (Ajuste de parámetros con error cuadrático)
L(θ)=u(t1;θ)uobs22L(\theta) = \| u(t_1; \theta) - u_{\text{obs}} \|_2^2

Tenemos que:


Derivación de la Ecuación de Sensibilidad

Para calcular S(t)=uθS(t) = \frac{\partial u}{\partial \theta}, aprovechamos la ODE original:

dudtf(u,t,θ)=0\frac{du}{dt} - f(u, t, \theta) = 0

Aplicamos la derivada parcial respecto a θ\theta:

θ(dudt)θ(f(u,t,θ))=0\frac{\partial}{\partial \theta}\left(\frac{du}{dt}\right) - \frac{\partial}{\partial \theta}\big(f(u, t, \theta)\big) = 0

y aprovechando que tienen la misma derivada y otras condiciones, podemos intercambiar el orden de derivación en el primer término:

θ(dudt)=ddt(uθ)=dSdt\frac{\partial}{\partial \theta}\left(\frac{du}{dt}\right) = \frac{d}{dt}\left(\frac{\partial u}{\partial \theta}\right) = \frac{dS}{dt}

Por otro lado, aplicamos regla de la cadena al segundo término:

θ(f(u,t,θ))=fuuθ+fθ=fuS+fθ\frac{\partial}{\partial \theta}\big(f(u, t, \theta)\big) = \frac{\partial f}{\partial u}\frac{\partial u}{\partial \theta} + \frac{\partial f}{\partial \theta} = \frac{\partial f}{\partial u} \cdot S + \frac{\partial f}{\partial \theta}

Entonces, volviendo a nuestra ecuación original, tenemos que:

θ(dudt)θ(f(u,t,θ))=dSdt(fuS+fθ)=0\frac{\partial}{\partial \theta}\left(\frac{du}{dt}\right) - \frac{\partial}{\partial \theta}\big(f(u, t, \theta)\big) = \frac{dS}{dt} - \left(\frac{\partial f}{\partial u} \cdot S + \frac{\partial f}{\partial \theta}\right) = 0

Y por lo tanto:

dSdt=fuS+fθ\frac{dS}{dt} = \frac{\partial f}{\partial u} \cdot S + \frac{\partial f}{\partial \theta}

Con la condición inicial:

S(t0)=u0θS(t_0) = \frac{\partial u_0}{\partial \theta}

Observación: En la mayoría de los casos S(t0)=0S(t_0) = 0 porque el estado inicial u0u_0 suele no depender de los parámetros que queremos optimizar.

Pros y Contras:

Pro:

Contra:

Importante: