Diferenciación automática en Julia: Zygote.jl, ForwardDiff y cómo Julia lo hace diferente

La diferenciación automática (AD) es lo que permite entrenar redes neuronales y optimizar funciones matemáticas sin derivar a mano. PyTorch y TensorFlow la implementan en C++. Julia la implementa en Julia, y eso tiene consecuencias prácticas: puedes diferenciar a través de cualquier función Julia, incluidos tus propios solvers de ecuaciones diferenciales o código de simulación física.

Dos modos de diferenciación automática

Hay dos formas de calcular gradientes automáticamente, y Julia tiene paquetes para cada una:

  • Forward mode (modo forward): calcula la derivada de una salida respecto a una entrada al mismo tiempo que evalúa la función. Eficiente cuando hay pocas entradas y muchas salidas.
  • Reverse mode (modo backward): hace un primer paso forward guardando el grafo de operaciones, luego un paso backward para propagar gradientes. Eficiente cuando hay muchas entradas y pocas salidas. Este es el que usa el backpropagation en redes neuronales.

ForwardDiff.jl: modo forward

ForwardDiff.jl implementa el modo forward con números duales. No construye ningún grafo: simplemente evalúa la función con números que llevan la derivada incorporada como segunda componente.

using ForwardDiff

# Gradiente de una función escalar
f(x) = x^3 + 2*x^2 - x + 1
df = ForwardDiff.derivative(f, 3.0)  # f'(3) = 3*9 + 4*3 - 1 = 38

# Gradiente de función multivariable
g(x) = x[1]^2 + x[1]*x[2] + x[2]^2
grad_g = ForwardDiff.gradient(g, [2.0, 3.0])  # [7.0, 7.0]

# Jacobiana (matriz de derivadas parciales)
h(x) = [x[1]^2 + x[2], x[1]*x[2]]
J = ForwardDiff.jacobian(h, [2.0, 3.0])
# Resultado: [[4.0, 1.0], [3.0, 2.0]]

# Hesiana (segunda derivada)
H = ForwardDiff.hessian(g, [2.0, 3.0])

ForwardDiff.jl es especialmente rápido para funciones con pocas entradas (uno o dos parámetros). El coste crece linealmente con el número de entradas, así que para redes neuronales con millones de parámetros no es la opción.

Zygote.jl: modo reverse

Zygote.jl es el motor de diferenciación de Flux.jl. Opera en modo reverse, lo que lo hace eficiente para funciones con muchas entradas (como los parámetros de una red neuronal) y una sola salida escalar (la función de pérdida).

using Zygote

# Gradiente básico
f(x) = x^3 + 2*x^2 - x + 1
grad, = gradient(f, 3.0)   # devuelve una tupla, tomamos el primer elemento
println("f'(3) = $grad")   # 38.0

# Gradiente multivariable
g(x, y) = x^2 + x*y + y^2
grad_x, grad_y = gradient(g, 2.0, 3.0)
println("?g/?x = $grad_x, ?g/?y = $grad_y")  # 7.0, 7.0

# Gradiente respecto a un array
h(w) = sum(w .^ 2)
grad_w, = gradient(h, [1.0, 2.0, 3.0])  # [2.0, 4.0, 6.0]

# Gradiente a través de operaciones de array
function perdida(W, x, y_target)
    y_pred = W * x
    return sum((y_pred .- y_target) .^ 2)
end

W = rand(3, 4)
x = rand(4)
y_target = rand(3)

grad_W, = gradient(W -> perdida(W, x, y_target), W)

Lo que diferencia a Zygote: código arbitrario

La ventaja real de Zygote es que puede diferenciar a través de código Julia arbitrario: bucles, condicionales, llamadas a funciones externas. No estás limitado a un conjunto de operaciones predefinidas.

using Zygote

# Función con bucle: Zygote lo diferencia correctamente
function suma_pesos(w, datos)
    resultado = 0.0
    for i in eachindex(datos)
        resultado += w[i] * datos[i]^2
    end
    return resultado
end

w = [1.0, 2.0, 3.0]
datos = [0.5, 1.5, 2.5]
grad_w, = gradient(w -> suma_pesos(w, datos), w)

# Función con condicional
function relu_manual(x)
    if x > 0
        return x
    else
        return 0.0
    end
end

grad_relu, = gradient(relu_manual, 3.0)   # 1.0
grad_relu2, = gradient(relu_manual, -2.0) # 0.0

Diferenciar a través de solvers de ecuaciones diferenciales

Aquí está la aplicación más potente y la que más diferencia a Julia de PyTorch. Con DifferentialEquations.jl y Zygote juntos puedes calcular gradientes a través de la solución de una EDO. Esto es la base de las «neural ordinary differential equations» y de la calibración de modelos físicos:

using DifferentialEquations, Zygote

# EDO: dy/dt = -p*y (decaimiento exponencial)
function ode!(du, u, p, t)
    du[1] = -p[1] * u[1]
end

# Función que resuelve la EDO y devuelve el valor final
function simular(p)
    u0 = [1.0]
    tspan = (0.0, 5.0)
    prob = ODEProblem(ode!, u0, tspan, p)
    sol  = solve(prob, Tsit5(), saveat=5.0)
    return sol[end][1]
end

# Gradiente de la solución respecto al parámetro p
# (necesario para ajustar p a datos experimentales)
p = [0.5]
grad_p, = gradient(p -> simular(p), p)
println("d(solución)/dp = $grad_p")

Optimización con gradientes

Con los gradientes disponibles, la optimización de funciones arbitrarias es directa. Optim.jl acepta funciones y sus gradientes calculados con ForwardDiff o Zygote:

using Optim, Zygote

# Función a minimizar (función de Rosenbrock)
function rosenbrock(x)
    return (1 - x[1])^2 + 100*(x[2] - x[1]^2)^2
end

# Optimizar con gradientes calculados por Zygote
resultado = optimize(
    rosenbrock,
    x -> gradient(rosenbrock, x)[1],   # gradiente
    [0.0, 0.0],                          # punto inicial
    LBFGS()
)

println("Mínimo en: ", Optim.minimizer(resultado))
println("Valor mínimo: ", Optim.minimum(resultado))

La diferenciación automática es la razón técnica de fondo de por qué Julia tiene sentido para ciencia computacional en 2026. El diseño del lenguaje, con el sistema de tipos y el multiple dispatch, permite que Zygote propague gradientes a través de código de terceros sin que los autores de ese código hayan tenido que hacer nada especial. Es la misma propiedad que hace que Flux.jl pueda diferenciar a través de capas personalizadas escritas por el usuario.

Esta combinación de AD nativo en el lenguaje, velocidad de código nativo y un ecosistema de librerías científicas maduro es lo que mantiene a Julia como una opción seria para investigación computacional, aunque Python y otros lenguajes sigan dominando el mercado general.

Imagen: Pexels / Monstera Production

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP