Dès le début, il a été passionnant de voir le nombre croissant de packages se développer dans le torch
écosystème. Ce qui est étonnant, c’est la variété des choses que les gens font avec torch
: étendre ses fonctionnalités ; intégrer et mettre à revenue dans un domaine spécifique son infrastructure de différenciation automatique de bas niveau ; architectures de réseaux de neurones portuaires… et final however not least, répondre à des questions scientifiques.
Ce billet de weblog présentera, sous une forme courte et plutôt subjective, l’un de ces packages : torchopt
. Avant de commencer, une selected que nous devrions probablement dire beaucoup plus souvent : si vous souhaitez publier un article sur ce weblog, sur le package deal que vous développez ou sur la manière dont vous utilisez les frameworks d’apprentissage en profondeur du langage R, faites-le nous savoir. – vous êtes plus que bienvenus!
torchopt
torchopt
est un package deal développé par Gilberto Camara et collègues de Institut nationwide de recherche spatiale, Brésil.
À première vue, la raison d’être de l’emballage est plutôt évidente. torch
lui-même n’implémente pas – et ne devrait pas non plus – mettre en œuvre tous les algorithmes d’optimisation récemment publiés et potentiellement utiles pour vos besoins. Les algorithmes rassemblés ici sont donc probablement exactement ceux que les auteurs avaient le plus envie d’expérimenter dans leur propre travail. Au second d’écrire ces lignes, ils comprennent, entre autres, divers membres du mouvement populaire ADA* et *ADAM* des familles. Et nous pouvons supposer en toute sécurité que la liste s’allongera avec le temps.
Je vais présenter le package deal en mettant en évidence quelque selected qui, techniquement, est « simplement » une fonction utilitaire, mais qui peut être extrêmement utile pour l’utilisateur : la possibilité, pour un optimiseur arbitraire et une fonction de take a look at arbitraire, de tracer les étapes suivies en optimisation.
S’il est vrai que je n’ai aucune intention de comparer (et encore moins d’analyser) différentes stratégies, il y en a une qui, pour moi, se démarque dans la liste : ADAHESSIAN (Yao et al. 2020), un algorithme de second ordre conçu pour s’adapter aux grands réseaux de neurones. Je suis particulièrement curieux de voir remark il se comporte par rapport au L-BFGS, le « classique » de second ordre disponible à partir de la base torch
nous avons eu un article de weblog dédié environ l’année dernière.
La façon dont ça marche
La fonction d’utilité en query est nommée test_optim()
. Le seul argument requis concerne l’optimiseur à essayer (optim
). Mais vous voudrez probablement en modifier trois autres également :
test_fn
: Pour utiliser une fonction de take a look at différente de celle par défaut (beale
). Vous pouvez choisir parmi les nombreux proposés danstorchopt
, ou vous pouvez passer le vôtre. Dans ce dernier cas, vous devez également fournir des informations sur le domaine de recherche et les factors de départ. (Nous verrons cela dans un instantaneous.)steps
: Pour définir le nombre d’étapes d’optimisation.opt_hparams
: Pour modifier les hyperparamètres de l’optimiseur ; plus particulièrement, le taux d’apprentissage.
Ici, je vais utiliser le flower()
fonction qui figurait déjà en bonne place dans le publish susmentionné sur L-BFGS. Il se rapproche de son minimal à mesure qu’il se rapproche de (0,0)
(mais est indéfini à l’origine elle-même).
C’est ici:
flower <- operate(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))
}
Pour voir à quoi ça ressemble, faites défiler un peu vers le bas. L’intrigue peut être modifiée de multiples façons, mais je m’en tiendrai à la disposition par défaut, avec des couleurs de longueur d’onde plus courte mappées sur des valeurs de fonction inférieures.
Commençons nos explorations.
Pourquoi disent-ils toujours que le taux d’apprentissage est necessary ?
C’est vrai que c’est une query rhétorique. Mais encore, parfois, les visualisations constituent les preuves les plus mémorables.
Ici, nous utilisons un optimiseur de premier ordre populaire, AdamW (Loshchilov et Hutter 2017). Nous l’appelons avec son taux d’apprentissage par défaut, 0.01
, et laissez la recherche s’exécuter sur deux cents étapes. Comme dans ce publish précédent, nous partons de loin – le level (20,20)
bien en dehors de la région rectangulaire d’intérêt.
library(torchopt)
library(torch)
test_optim(
# name with default studying price (0.01)
optim = optim_adamw,
# move in self-defined take a look at operate, plus a closure indicating beginning factors and search area
test_fn = checklist(flower, operate() (c(x0 = 20, y0 = 20, xmax = 3, xmin = -3, ymax = 3, ymin = -3))),
steps = 200
)

Oups, que s’est-il passé ? Y a-t-il une erreur dans le code de traçage ? – Pas du tout; c’est juste qu’après le nombre most d’étapes autorisées, nous ne sommes pas encore entrés dans la région d’intérêt.
Ensuite, nous augmentons le taux d’apprentissage d’un facteur dix.

Quel changement ! Avec un taux d’apprentissage multiplié par dix, le résultat est optimum. Cela signifie-t-il que le paramètre par défaut est mauvais ? Bien sûr que non; l’algorithme a été réglé pour bien fonctionner avec les réseaux de neurones, et non avec une fonction qui a été délibérément conçue pour présenter un défi spécifique.
Naturellement, nous devons également voir ce qui se passe pour un taux d’apprentissage encore plus élevé.

Nous voyons le comportement dont nous avons toujours été avertis : l’optimisation sautille follement, avant de sembler s’éterniser. (Apparemment, parce que dans ce cas, ce n’est pas ce qui se passe. Au lieu de cela, la recherche sautera loin, et reviendra, en continu.)
Maintenant, cela pourrait rendre un curieux. Que se passe-t-il réellement si nous choisissons le « bon » taux d’apprentissage, mais ne nous arrêtons pas à l’optimisation à deux cents étapes ? Ici, nous essayons trois cents à la place :

Fait intéressant, nous voyons le même style de va-et-vient se produire ici qu’avec un taux d’apprentissage plus élevé – c’est juste retardé dans le temps.
Une autre query amusante qui vient à l’esprit est la suivante : pouvons-nous suivre la manière dont le processus d’optimisation « discover » les quatre pétales ? Avec quelques expérimentations rapides, je suis arrivé à ceci:

Qui a dit qu’il fallait du chaos pour produire une belle intrigue ?
Un optimiseur de second ordre pour les réseaux de neurones : ADAHESSIAN
Passons à l’algorithme que j’aimerais vérifier spécifiquement. À la suite d’un peu d’expérimentation sur le taux d’apprentissage, j’ai pu arriver à un wonderful résultat après seulement trente-cinq étapes.

Compte tenu de nos expériences récentes avec AdamW, c’est-à-dire qu’il « ne s’installe tout simplement pas » très proche du minimal, nous voudrons peut-être également effectuer un take a look at équivalent avec ADAHESSIAN. Que se passe-t-il si nous continuons à optimiser un peu plus longtemps – pour deux cents étapes, disons ?

Comme AdamW, ADAHESSIAN va « explorer » les pétales, mais il ne s’éloigne pas pour autant du minimal.
Est-ce surprenant ? Je ne dirais pas que c’est le cas. L’argument est le même qu’avec AdamW, ci-dessus : son algorithme a été réglé pour bien fonctionner sur de grands réseaux de neurones, et non pour résoudre une tâche de minimisation classique et artisanale.
Maintenant que nous avons déjà entendu cet argument deux fois, il est temps de vérifier l’hypothèse explicite : qu’un algorithme classique du second ordre gère mieux cela. En d’autres termes, il est temps de revisiter L-BFGS.
Le meilleur des classiques : revisiter le L-BFGS
Utiliser test_optim()
avec L-BFGS, il faut faire un petit détour. Si vous avez lu le message sur L-BFGS, vous vous souvenez peut-être qu’avec cet optimiseur, il est nécessaire d’envelopper à la fois l’appel à la fonction de take a look at et l’évaluation du gradient dans une fermeture. (La raison étant que les deux doivent être appelables plusieurs fois par itération.)
Maintenant, vu remark L-BFGS est un cas très spécial, et peu de gens sont susceptibles d’utiliser test_optim()
avec elle à l’avenir, il ne semblerait pas utile de faire en sorte que cette fonction gère différents cas. Pour ce take a look at on-off, j’ai simplement copié et modifié le code selon les besoins. Le résultat, test_optim_lbfgs()
se trouve dans le annexe.
En décidant du nombre d’étapes à essayer, nous tenons compte du fait que L-BFGS a un idea d’itérations différent de celui des autres optimiseurs ; c’est-à-dire qu’il peut affiner sa recherche plusieurs fois par étape. En effet, du publish précédent je sais que trois itérations suffisent :

À ce stade, bien sûr, je dois m’en tenir à ma règle de tester ce qui se passe avec « trop d’étapes ». (Même si cette fois, j’ai de bonnes raisons de croire qu’il ne se passera rien.)

Hypothèse confirmée.
Et ici se termine mon introduction ludique et subjective à torchopt
. J’espère que vous l’avez aimé; mais en tout cas, je pense que vous auriez dû avoir l’impression qu’il s’agit ici d’un package deal utile, extensible et inclined d’évoluer, à surveiller à l’avenir. Comme toujours, merci d’avoir lu!
annexe
test_optim_lbfgs <- operate(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 operate
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 operate name and gradient analysis in a closure,
# for them to be callable a number of occasions per iteration.
calc_loss <- operate() {
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 = operate(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 et Frank Hutter. 2017. « Fixation de la régularisation de la carie de poids chez Adam. » CoRR abs/1711.05101. http://arxiv.org/abs/1711.05101.
Yao, Zhewei, Amir Gholami, Sheng Shen, Kurt Keutzer et Michael W. Mahoney. 2020. « ADAHESSIAN : un optimiseur adaptatif de second ordre pour l’apprentissage automatique. » CoRR abs/2006.00719. https://arxiv.org/abs/2006.00719.