Desde o início, tem sido emocionante observar o número crescente de pacotes sendo desenvolvidos no torch
ecossistema. O que é incrível é a variedade de coisas que as pessoas fazem com torch
: ampliar sua funcionalidade; integrar e colocar em uso específico de domínio sua infra-estrutura de diferenciação automática de baixo nível; arquiteturas de redes neurais portuárias… e por último, mas não menos importante, responder a questões científicas.
Esta postagem do weblog apresentará, de forma curta e bastante subjetiva, um destes pacotes: torchopt
. Antes de começarmos, uma coisa que provavelmente deveríamos dizer com mais frequência: se você quiser publicar uma postagem neste weblog, sobre o pacote que está desenvolvendo ou sobre a maneira como você emprega estruturas de aprendizado profundo em linguagem R, informe-nos – você é mais que bem-vindo!
torchopt
torchopt
é um pacote desenvolvido por Gilberto Câmara e colegas em Instituto Nacional de Pesquisas Espaciais, Brasil.
Ao que parece, a razão de ser do pacote é bastante evidente. torch
em si não implementa – nem deveria – implementar todos os algoritmos de otimização recém-publicados e potencialmente úteis para seus propósitos. Os algoritmos aqui reunidos são provavelmente exatamente aqueles que os autores estavam mais ansiosos para experimentar em seu próprio trabalho. No momento em que este livro foi escrito, eles incluíam, entre outros, vários membros da comunidade fashionable ADA* e *ADÃO*famílias. E podemos assumir com segurança que a lista aumentará com o tempo.
Vou apresentar o pacote destacando algo que tecnicamente é “meramente” uma função utilitária, mas para o usuário pode ser extremamente útil: a capacidade de, para um otimizador arbitrário e uma função de teste arbitrária, traçar os passos dados em otimização.
Embora seja verdade que não tenho intenção de comparar (e muito menos analisar) diferentes estratégias, há uma que, para mim, se destaca na lista: ADAHESSIAN (Yao et al. 2020)um algoritmo de segunda ordem projetado para escalar grandes redes neurais. Estou especialmente curioso para ver como ele se comporta em comparação com o L-BFGS, o “clássico” de segunda ordem disponível na base torch
tivemos um postagem de weblog dedicada sobre o ano passado.
A forma como funciona
A função utilidade em questão é chamada test_optim()
. O único argumento obrigatório diz respeito ao otimizador para tentar (optim
). Mas você provavelmente desejará ajustar outros três também:
test_fn
: Para usar uma função de teste diferente do padrão (beale
). Você pode escolher entre os muitos fornecidos emtorchopt
ou você pode passar por conta própria. Neste último caso, você também precisa fornecer informações sobre o domínio de pesquisa e os pontos de partida. (Veremos isso em um instante.)steps
: para definir o número de etapas de otimização.opt_hparams
: Para modificar hiperparâmetros do otimizador; mais notavelmente, a taxa de aprendizagem.
Aqui, vou usar o flower()
função que já figurava com destaque na postagem mencionada em L-BFGS. Aproxima-se do seu mínimo à medida que se aproxima cada vez mais de (0,0)
(mas é indefinido na própria origem).
Aqui está:
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 como fica, basta rolar um pouco para baixo. O gráfico pode ser ajustado de inúmeras maneiras, mas continuarei com o format padrão, com cores de comprimento de onda mais curto mapeadas para valores de função mais baixos.
Vamos começar nossas explorações.
Por que eles sempre dizem que a taxa de aprendizagem é importante?
É verdade que é uma pergunta retórica. Mesmo assim, às vezes as visualizações constituem as evidências mais memoráveis.
Aqui, usamos um fashionable otimizador de primeira ordem, AdamW (Loschilov e Hutter 2017). Chamamos isso de sua taxa de aprendizado padrão, 0.01
e deixe a pesquisa durar duzentas etapas. Como no put up anterior, começamos de longe – o ponto (20,20)
bem fora da região retangular de interesse.
library(torchopt)
library(torch)
test_optim(
# name with default studying fee (0.01)
optim = optim_adamw,
# move in self-defined take a look at perform, plus a closure indicating beginning factors and search area
test_fn = checklist(flower, perform() (c(x0 = 20, y0 = 20, xmax = 3, xmin = -3, ymax = 3, ymin = -3))),
steps = 200
)

Opa, o que aconteceu? Existe um erro no código de plotagem? – De jeito nenhum; só que após o número máximo de passos permitidos, ainda não entramos na região de interesse.
Em seguida, aumentamos a taxa de aprendizagem por um fator de dez.

Que mudança! Com uma taxa de aprendizado dez vezes maior, o resultado é ótimo. Isso significa que a configuração padrão é ruim? Claro que não; o algoritmo foi ajustado para funcionar bem com redes neurais, e não com alguma função que foi projetada propositalmente para apresentar um desafio específico.
Naturalmente, também temos que ver o que acontece com uma taxa de aprendizagem ainda mais elevada.

Vemos o comportamento sobre o qual sempre fomos alertados: a otimização salta descontroladamente, antes de aparentemente desaparecer para sempre. (Aparentemente, porque neste caso, não é isso que acontece. Em vez disso, a pesquisa irá saltar para longe e voltar novamente, continuamente.)
Agora, isso pode deixar alguém curioso. O que realmente acontece se escolhermos a “boa” taxa de aprendizado, mas não pararmos de otimizar em duzentos passos? Aqui, tentamos trezentos:

Curiosamente, vemos aqui o mesmo tipo de vaivém que ocorre com uma taxa de aprendizagem mais alta – apenas atrasado no tempo.
Outra questão divertida que vem à mente é: podemos acompanhar como o processo de otimização “explora” as quatro pétalas? Com algumas experiências rápidas, cheguei a isto:

Quem disse que você precisa do caos para produzir um enredo bonito?
Um otimizador de segunda ordem para redes neurais: ADAHESSIAN
Vamos ao único algoritmo que gostaria de verificar especificamente. Após um pouco de experimentação na taxa de aprendizagem, consegui chegar a um resultado excelente após apenas trinta e cinco etapas.

Dadas as nossas experiências recentes com AdamW – ou seja, “simplesmente não se adapta” muito perto do mínimo – podemos querer realizar um teste equivalente com ADAHESSIAN também. O que acontece se continuarmos otimizando um pouco mais – por duzentos passos, digamos?

Assim como AdamW, ADAHESSIAN passa a “explorar” as pétalas, mas não se afasta tanto do mínimo.
Isso é surpreendente? Eu não diria que é. O argumento é o mesmo do AdamW acima: seu algoritmo foi ajustado para funcionar bem em grandes redes neurais, e não para resolver uma tarefa de minimização clássica e artesanal.
Agora que já ouvimos esse argumento duas vezes, é hora de verificar a suposição explícita: que um algoritmo clássico de segunda ordem lida melhor com isso. Em outras palavras, é hora de revisitar o L-BFGS.
O melhor dos clássicos: revisitando L-BFGS
Para usar test_optim()
com o L-BFGS, precisamos fazer um pequeno desvio. Se você leu a postagem em L-BFGSvocê deve se lembrar que com esse otimizador é necessário agrupar a chamada para a função de teste e a avaliação do gradiente em um fechamento. (A razão é que ambos precisam ser chamados várias vezes por iteração.)
Agora, vendo como o L-BFGS é um caso muito especial, e poucas pessoas provavelmente usarão test_optim()
com isso no futuro, não pareceria valer a pena fazer com que essa função lidasse com casos diferentes. Para este teste liga-desliga, simplesmente copiei e modifiquei o código conforme necessário. O resultado, test_optim_lbfgs()
é encontrado no apêndice.
Ao decidir quantas etapas tentar, levamos em consideração que L-BFGS tem um conceito de iterações diferente de outros otimizadores; ou seja, pode refinar sua pesquisa várias vezes por etapa. Na verdade, na postagem anterior, sei que três iterações são suficientes:

Neste ponto, é claro, preciso seguir minha regra de testar o que acontece com “demasiadas etapas”. (Mesmo que desta vez eu tenha fortes razões para acreditar que nada acontecerá.)

Hipótese confirmada.
E aqui termina minha introdução lúdica e subjetiva ao torchopt
. Eu certamente espero que você tenha gostado; mas, de qualquer forma, acho que você deveria ter ficado com a impressão de que aqui está um pacote útil, extensível e com probabilidade de crescimento, que deve ser observado no futuro. Como sempre, obrigado pela leitura!
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.checklist(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(checklist(params = checklist(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
strains(
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 e Frank Hutter. 2017. “Consertando a regularização da queda de peso em Adam.” CoRR abs/1711.05101. http://arxiv.org/abs/1711.05101.
Yao, Zhewei, Amir Gholami, Sheng Shen, Kurt Keutzer e Michael W. Mahoney. 2020. “ADAHESSIAN: Um otimizador adaptativo de segunda ordem para aprendizado de máquina.” CoRR abs/2006.00719. https://arxiv.org/abs/2006.00719.