24 C
Colombia
lunes, julio 7, 2025

Weblog de Posit AI: Destacado de la comunidad: diversión con torchopt


Desde el principio, ha sido emocionante observar el creciente número de paquetes que se desarrollan en el torch ecosistema. Lo sorprendente es la variedad de cosas que la gente hace con torch: ampliar su funcionalidad; integrar y utilizar en dominios específicos su infraestructura de diferenciación automática de bajo nivel; portar arquitecturas de redes neuronales… y por último, pero no menos importante, responder preguntas científicas.

Esta publicación de weblog presentará, de forma breve y bastante subjetiva, uno de estos paquetes: torchopt. Antes de comenzar, una cosa que probablemente deberíamos decir con mucha más frecuencia: si desea publicar una publicación en este weblog, sobre el paquete que está desarrollando o la forma en que emplea los marcos de aprendizaje profundo en lenguaje R, háganoslo saber. – ¡eres más que bienvenido!

torchopt

torchopt es un paquete desarrollado por Gilberto Camara y colegas en Instituto Nacional de Investigaciones Espaciales, Brasil.

A primera vista, la razón de ser del paquete es bastante evidente. torch por sí mismo no implementa (ni debería implementar) todos los algoritmos de optimización potencialmente útiles para sus propósitos recientemente publicados que existen. Los algoritmos reunidos aquí, entonces, son probablemente exactamente aquellos con los que los autores estaban más ansiosos por experimentar en su propio trabajo. Al momento de escribir estas líneas, comprenden, entre otros, varios miembros del grupo in style ADA* y *ADÁN* familias. Y podemos asumir con seguridad que la lista crecerá con el tiempo.

Voy a presentar el paquete destacando algo que técnicamente es “simplemente” una función de utilidad, pero que para el usuario puede ser extremadamente útil: la capacidad de, para un optimizador arbitrario y una función de prueba arbitraria, trazar los pasos tomados. en optimización.

Si bien es cierto que no tengo intención de comparar (y mucho menos analizar) diferentes estrategias, hay una que, para mí, destaca en la lista: ADAHESSIAN (Yao et al. 2020)un algoritmo de segundo orden diseñado para escalar a grandes redes neuronales. Tengo especial curiosidad por ver cómo se comporta en comparación con el L-BFGS, el “clásico” de segundo orden disponible desde la base. torch hemos tenido un publicación de weblog dedicada sobre el año pasado.

La forma en que funciona

La función de utilidad en cuestión se denomina test_optim(). El único argumento requerido se refiere al optimizador que debe intentar (optim). Pero probablemente también querrás modificar otros tres:

  • test_fn: Para utilizar una función de prueba diferente a la predeterminada (beale). Puede elegir entre los muchos proporcionados en torchopto puedes pasar el tuyo propio. En este último caso, también deberá proporcionar información sobre el dominio de búsqueda y los puntos de partida. (Lo veremos en un instante).
  • steps: Para establecer el número de pasos de optimización.
  • opt_hparams: Para modificar los hiperparámetros del optimizador; más notablemente, la tasa de aprendizaje.

Aquí voy a usar el flower() función que ya ocupaba un lugar destacado en el put up antes mencionado sobre L-BFGS. Se acerca a su mínimo a medida que se acerca cada vez más a (0,0) (pero no está definido en el origen mismo).

Aquí lo tienes:

flower <- perform(x, y) {
  a <- 1
  b <- 1
  c <- 4
  a * torch_sqrt(torch_square(x) + torch_square(y)) + b * torch_sin(c * torch_atan2(y, x))
}

Para ver cómo se ve, simplemente desplácese un poco hacia abajo. La trama se puede modificar de muchas maneras, pero me quedaré con el diseño predeterminado, con colores de longitud de onda más corta asignados a valores de función más bajos.

Comencemos nuestras exploraciones.

¿Por qué siempre dicen que la tasa de aprendizaje es importante?

Es cierto que es una pregunta retórica. Pero aún así, a veces las visualizaciones constituyen la evidencia más memorable.

Aquí utilizamos un in style optimizador de primer orden, AdamW. (Loshchilov y Hutter 2017). Lo llamamos con su tasa de aprendizaje predeterminada, 0.01y deje que la búsqueda se ejecute durante doscientos pasos. Como en el put up anterior, empezamos desde muy lejos – el punto (20,20)muy fuera de la región rectangular de interés.

library(torchopt)
library(torch)

test_optim(
    # name with default studying fee (0.01)
    optim = optim_adamw,
    # cross in self-defined take a look at perform, plus a closure indicating beginning factors and search area
    test_fn = listing(flower, perform() (c(x0 = 20, y0 = 20, xmax = 3, xmin = -3, ymax = 3, ymin = -3))),
    steps = 200
)
Minimizando la función de la flor con AdamW. Configuración no. 1: tasa de aprendizaje predeterminada, 200 pasos.

Ups, ¿qué pasó? ¿Hay algún error en el código de trazado? – De nada; es solo que después del número máximo de pasos permitidos, aún no hemos ingresado a la región de interés.

A continuación, aumentamos la tasa de aprendizaje en un issue de diez.

test_optim(
    optim = optim_adamw,
    # scale default fee by an element of 10
    opt_hparams = listing(lr = 0.1),
    test_fn = listing(flower, perform() (c(x0 = 20, y0 = 20, xmax = 3, xmin = -3, ymax = 3, ymin = -3))),
    steps = 200
)
Minimizando la función de la flor con AdamW. Configuración no. 1: tasa de aprendizaje predeterminada, 200 pasos.

¡Qué cambio! Con una tasa de aprendizaje diez veces mayor, el resultado es óptimo. ¿Significa esto que la configuración predeterminada es mala? Por supuesto que no; el algoritmo ha sido ajustado para funcionar bien con redes neuronales, no con alguna función que haya sido diseñada específicamente para presentar un desafío específico.

Naturalmente, también tenemos que ver qué sucede con una tasa de aprendizaje aún mayor.

test_optim(
    optim = optim_adamw,
    # scale default fee by an element of 70
    opt_hparams = listing(lr = 0.7),
    test_fn = listing(flower, perform() (c(x0 = 20, y0 = 20, xmax = 3, xmin = -3, ymax = 3, ymin = -3))),
    steps = 200
)
Minimizando la función de la flor con AdamW. Configuración no. 3: lr = 0,7, 200 pasos.

Vemos el comportamiento sobre el que siempre nos han advertido: la optimización salta salvajemente, antes de aparentemente irse para siempre. (Aparentemente, porque en este caso, esto no es lo que sucede. En cambio, la búsqueda se alejará y retrocederá continuamente).

Ahora bien, esto podría generar curiosidad. ¿Qué sucede realmente si elegimos la tasa de aprendizaje “buena”, pero no dejamos de optimizar en doscientos pasos? Aquí probamos con trescientos:

test_optim(
    optim = optim_adamw,
    # scale default fee by an element of 10
    opt_hparams = listing(lr = 0.1),
    test_fn = listing(flower, perform() (c(x0 = 20, y0 = 20, xmax = 3, xmin = -3, ymax = 3, ymin = -3))),
    # this time, proceed search till we attain step 300
    steps = 300
)
Minimizando la función de la flor con AdamW. Configuración no. 3: lr

Curiosamente, vemos que aquí ocurre el mismo tipo de vaivén que con una tasa de aprendizaje más alta: solo que se retrasa en el tiempo.

Otra pregunta divertida que me viene a la mente es: ¿Podemos rastrear cómo el proceso de optimización “explora” los cuatro pétalos? Con un poco de experimentación rápida, llegué a esto:

Minimizando la función floral con AdamW, lr = 0,1: “Exploración” sucesiva de pétalos. Pasos (en el sentido de las agujas del reloj): 300, 700, 900, 1300.

¿Quién cube que se necesita caos para producir una trama hermosa?

Un optimizador de segundo orden para redes neuronales: ADAHESSIAN

Pasemos al algoritmo que me gustaría comprobar específicamente. Después de un poco de experimentación con el ritmo de aprendizaje, pude llegar a un resultado excelente después de sólo treinta y cinco pasos.

test_optim(
    optim = optim_adahessian,
    opt_hparams = listing(lr = 0.3),
    test_fn = listing(flower, perform() (c(x0 = 20, y0 = 20, xmax = 3, xmin = -3, ymax = 3, ymin = -3))),
    steps = 35
)
Minimizando la función de la flor con AdamW. Configuración no. 3: lr

Sin embargo, dadas nuestras experiencias recientes con AdamW (es decir, “simplemente no se adapta” muy cerca del mínimo), es posible que también queramos realizar una prueba equivalente con ADAHESSIAN. ¿Qué sucede si seguimos optimizando un poco más de tiempo (por ejemplo, doscientos pasos)?

test_optim(
    optim = optim_adahessian,
    opt_hparams = listing(lr = 0.3),
    test_fn = listing(flower, perform() (c(x0 = 20, y0 = 20, xmax = 3, xmin = -3, ymax = 3, ymin = -3))),
    steps = 200
)
Minimizando la función de la flor con ADAHESSIAN. Configuración no. 2: lr = 0,3, 200 pasos.

Al igual que AdamW, ADAHESSIAN continúa “explorando” los pétalos, pero no se aleja tanto del mínimo.

¿Es esto sorprendente? Yo no diría que lo es. El argumento es el mismo que con AdamW, arriba: su algoritmo ha sido ajustado para funcionar bien en grandes redes neuronales, no para resolver una tarea de minimización clásica hecha a mano.

Ahora que ya hemos escuchado ese argumento dos veces, es hora de verificar la suposición explícita: que un algoritmo clásico de segundo orden maneja esto mejor. En otras palabras, es hora de revisar L-BFGS.

Lo mejor de los clásicos: revisitando L-BFGS

para usar test_optim() con L-BFGS, necesitamos tomar un pequeño desvío. Si has leído la publicación en L-BFGSquizás recuerdes que con este optimizador, es necesario envolver tanto la llamada a la función de prueba como la evaluación del gradiente en un cierre. (La razón es que ambos deben poder invocarse varias veces por iteración).

Ahora bien, al ver que L-BFGS es un caso muy especial y es possible que pocas personas lo utilicen test_optim() con él en el futuro, no parecería que valga la pena hacer que esa función maneje casos diferentes. Para esta prueba intermitente, simplemente copié y modifiqué el código según fuera necesario. El resultado, test_optim_lbfgs()se encuentra en el apéndice.

Al decidir qué número de pasos probar, tomamos en cuenta que L-BFGS tiene un concepto de iteraciones diferente al de otros optimizadores; es decir, puede refinar su búsqueda varias veces por paso. De hecho, por la publicación anterior sé que tres iteraciones son suficientes:

test_optim_lbfgs(
    optim = optim_lbfgs,
    opt_hparams = listing(line_search_fn = "strong_wolfe"),
    test_fn = listing(flower, perform() (c(x0 = 20, y0 = 20, xmax = 3, xmin = -3, ymax = 3, ymin = -3))),
    steps = 3
)
Minimizando la función de la flor con L-BFGS. Configuración no. 1: 3 pasos.

En este punto, por supuesto, debo ceñirme a mi regla de probar lo que sucede con “demasiados pasos”. (Aunque esta vez tengo fuertes razones para creer que no pasará nada).

test_optim_lbfgs(
    optim = optim_lbfgs,
    opt_hparams = listing(line_search_fn = "strong_wolfe"),
    test_fn = listing(flower, perform() (c(x0 = 20, y0 = 20, xmax = 3, xmin = -3, ymax = 3, ymin = -3))),
    steps = 10
)
Minimizando la función de la flor con L-BFGS. Configuración no. 2: 10 pasos.

Hipótesis confirmada.

Y aquí termina mi divertida y subjetiva introducción a torchopt. Ciertamente espero que les haya gustado; pero en cualquier caso, creo que debería haber tenido la impresión de que se trata de un paquete útil, extensible y con probabilidades de crecer, al que habrá que prestar atención en el futuro. Como siempre, ¡gracias por leer!

Apéndice

test_optim_lbfgs <- perform(optim, ...,
                       opt_hparams = NULL,
                       test_fn = "beale",
                       steps = 200,
                       pt_start_color = "#5050FF7F",
                       pt_end_color = "#FF5050FF",
                       ln_color = "#FF0000FF",
                       ln_weight = 2,
                       bg_xy_breaks = 100,
                       bg_z_breaks = 32,
                       bg_palette = "viridis",
                       ct_levels = 10,
                       ct_labels = FALSE,
                       ct_color = "#FFFFFF7F",
                       plot_each_step = FALSE) {


    if (is.character(test_fn)) {
        # get beginning factors
        domain_fn <- get(paste0("domain_",test_fn),
                         envir = asNamespace("torchopt"),
                         inherits = FALSE)
        # get gradient perform
        test_fn <- get(test_fn,
                       envir = asNamespace("torchopt"),
                       inherits = FALSE)
    } else if (is.listing(test_fn)) {
        domain_fn <- test_fn[[2]]
        test_fn <- test_fn[[1]]
    }

    # start line
    dom <- domain_fn()
    x0 <- dom[["x0"]]
    y0 <- dom[["y0"]]
    # create tensor
    x <- torch::torch_tensor(x0, requires_grad = TRUE)
    y <- torch::torch_tensor(y0, requires_grad = TRUE)

    # instantiate optimizer
    optim <- do.name(optim, c(listing(params = listing(x, y)), opt_hparams))

    # with L-BFGS, it's essential to wrap each perform name and gradient analysis in a closure,
    # for them to be callable a number of occasions per iteration.
    calc_loss <- perform() {
      optim$zero_grad()
      z <- test_fn(x, y)
      z$backward()
      z
    }

    # run optimizer
    x_steps <- numeric(steps)
    y_steps <- numeric(steps)
    for (i in seq_len(steps)) {
        x_steps[i] <- as.numeric(x)
        y_steps[i] <- as.numeric(y)
        optim$step(calc_loss)
    }

    # put together plot
    # get xy limits

    xmax <- dom[["xmax"]]
    xmin <- dom[["xmin"]]
    ymax <- dom[["ymax"]]
    ymin <- dom[["ymin"]]

    # put together knowledge for gradient plot
    x <- seq(xmin, xmax, size.out = bg_xy_breaks)
    y <- seq(xmin, xmax, size.out = bg_xy_breaks)
    z <- outer(X = x, Y = y, FUN = perform(x, y) as.numeric(test_fn(x, y)))

    plot_from_step <- steps
    if (plot_each_step) {
        plot_from_step <- 1
    }

    for (step in seq(plot_from_step, steps, 1)) {

        # plot background
        picture(
            x = x,
            y = y,
            z = z,
            col = hcl.colours(
                n = bg_z_breaks,
                palette = bg_palette
            ),
            ...
        )

        # plot contour
        if (ct_levels > 0) {
            contour(
                x = x,
                y = y,
                z = z,
                nlevels = ct_levels,
                drawlabels = ct_labels,
                col = ct_color,
                add = TRUE
            )
        }

        # plot start line
        factors(
            x_steps[1],
            y_steps[1],
            pch = 21,
            bg = pt_start_color
        )

        # plot path line
        traces(
            x_steps[seq_len(step)],
            y_steps[seq_len(step)],
            lwd = ln_weight,
            col = ln_color
        )

        # plot finish level
        factors(
            x_steps[step],
            y_steps[step],
            pch = 21,
            bg = pt_end_color
        )
    }
}

Loshchilov, Ilya y Frank Hutter. 2017. “Solucionar la regularización de la caída de peso en Adán”. CORR abs/1711.05101. http://arxiv.org/abs/1711.05101.

Yao, Zhewei, Amir Gholami, Sheng Shen, Kurt Keutzer y Michael W. Mahoney. 2020. “ADAHESSIAN: un optimizador adaptativo de segundo orden para el aprendizaje automático”. CORR abs/2006.00719. https://arxiv.org/abs/2006.00719.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles