Blog RStudio AI : Que la lumière soit : Plus de lumière pour la torche !

… Avant de commencer, mes excuses à nos lecteurs hispanophones … J’ai dû faire un choix entre « haja » et « foin »et à la fin tout dépendait d’un tirage au sort…

Au moment où j’écris ces lignes, nous sommes plus que satisfaits de l’adoption rapide que nous avons constatée torch – non seulement pour une utilisation immédiate, mais aussi, dans des packages qui s’en inspirent, en utilisant ses fonctionnalités de base.

Dans un scénario appliqué, cependant – un scénario qui implique la formation et la validation en parallèle, le calcul des métriques et l’action sur celles-ci, et la modification dynamique des hyper-paramètres au cours du processus – il peut parfois sembler qu’il y ait une quantité non négligeable de code passe-partout impliqué. D’une part, il y a la boucle principale sur les époques, et à l’intérieur, les boucles sur les lots d’apprentissage et de validation. De plus, des étapes telles que la mise à jour du modèle mode (formation ou validation, resp.), la mise à zéro et le calcul des gradients, et la propagation des mises à jour du modèle doivent être effectuées dans le bon ordre. Enfin et surtout, il faut veiller à ce qu’à tout moment, les tenseurs soient situés sur le dispositif.

Ne serait-ce pas rêveur si, comme le disait la populaire série « Head First… » du début des années 2000, il y avait un moyen d’éliminer ces étapes manuelles, tout en gardant la flexibilité ? Avec luzil y a.

Dans cet article, nous nous concentrons sur deux choses : tout d’abord, le flux de travail rationalisé lui-même ; et deuxièmement, des mécanismes génériques qui permettent la personnalisation. Pour des exemples plus détaillés de ce dernier, ainsi que des instructions de codage concrètes, nous établirons un lien vers la documentation (déjà complète).

Entraînez et validez, puis testez : un flux de travail d’apprentissage en profondeur de base avec luz

Pour démontrer le flux de travail essentiel, nous utilisons un ensemble de données facilement disponible et qui ne nous distraira pas trop, du point de vue du prétraitement : à savoir, le Chiens contre chats collection qui vient avec torchdatasets. torchvision sera nécessaire pour les transformations d’image ; en dehors de ces deux packages, tout ce dont nous avons besoin est torch et luz.

Données

L’ensemble de données est téléchargé depuis Kaggle ; vous devrez modifier le chemin ci-dessous pour refléter l’emplacement de votre propre jeton Kaggle.

dir <- "~/Downloads/dogs-vs-cats" 

ds <- torchdatasets::dogs_vs_cats_dataset(
  dir,
  token = "~/.kaggle/kaggle.json",
  transform = . %>%
    torchvision::transform_to_tensor() %>%
    torchvision::transform_resize(size = c(224, 224)) %>% 
    torchvision::transform_normalize(rep(0.5, 3), rep(0.5, 3)),
  target_transform = function(x) as.double(x) - 1
)

De manière pratique, nous pouvons utiliser dataset_subset() pour partitionner les données en ensembles de formation, de validation et de test.

train_ids <- sample(1:length(ds), size = 0.6 * length(ds))
valid_ids <- sample(setdiff(1:length(ds), train_ids), size = 0.2 * length(ds))
test_ids <- setdiff(1:length(ds), union(train_ids, valid_ids))

train_ds <- dataset_subset(ds, indices = train_ids)
valid_ds <- dataset_subset(ds, indices = valid_ids)
test_ds <- dataset_subset(ds, indices = test_ids)

Ensuite, nous instancions les dataloaders.

train_dl <- dataloader(train_ds, batch_size = 64, shuffle = TRUE, num_workers = 4)
valid_dl <- dataloader(valid_ds, batch_size = 64, num_workers = 4)
test_dl <- dataloader(test_ds, batch_size = 64, num_workers = 4)

C’est tout pour les données – aucun changement dans le flux de travail jusqu’à présent. Il n’y a pas non plus de différence dans la façon dont nous définissons le modèle.

Modèle

Pour accélérer la formation, nous nous appuyons sur AlexNet pré-formé ( Krijevski (2014)).

net <- torch::nn_module(
  
  initialize = function(output_size) {
    self$model <- model_alexnet(pretrained = TRUE)

    for (par in self$parameters) {
      par$requires_grad_(FALSE)
    }

    self$model$classifier <- nn_sequential(
      nn_dropout(0.5),
      nn_linear(9216, 512),
      nn_relu(),
      nn_linear(512, 256),
      nn_relu(),
      nn_linear(256, output_size)
    )
  },
  forward = function(x) {
    self$model(x)[,1]
  }
  
)

Si vous regardez attentivement, vous voyez que tout ce que nous avons fait jusqu’à présent est définir le modèle. Contrairement à un torch-uniquement workflow, nous n’allons pas l’instancier, et nous n’allons pas non plus le déplacer vers un éventuel GPU.

En développant ce dernier, nous pouvons en dire plus: Tout de la manipulation des appareils est gérée par luz. Il recherche l’existence d’un GPU compatible CUDA et, s’il en trouve un, s’assure que les poids du modèle et les tenseurs de données y sont déplacés de manière transparente chaque fois que nécessaire. Il en va de même pour la direction opposée : les prédictions calculées sur l’ensemble de test, par exemple, sont transférées silencieusement vers le CPU, prêtes à être manipulées par l’utilisateur dans R. Mais en ce qui concerne les prédictions, nous n’en sommes pas encore là : sur modéliser la formation, où la différence faite par luz saute droit aux yeux.

Entraînement

Ci-dessous, vous voyez quatre appels à luz, dont deux sont obligatoires dans chaque contexte, et deux dépendent de la casse. Ceux qui sont toujours nécessaires sont setup() et fit() :

  • Dans setup()tu dis luz quelle devrait être la perte et quel optimiseur utiliser. En option, au-delà de la perte elle-même (la mesure principale, en un sens, en ce sens qu’elle informe de la mise à jour du poids), vous pouvez avoir luz en calculer d’autres. Ici, par exemple, nous demandons la précision de la classification. (Pour un humain regardant une barre de progression, une précision à deux classes de 0,91 est bien plus indicative qu’une perte d’entropie croisée de 1,26.)

  • Dans fit()vous passez des références à la formation et à la validation dataloaders. Bien qu’il existe une valeur par défaut pour le nombre d’époques pour lesquelles s’entraîner, vous souhaiterez normalement également transmettre une valeur personnalisée pour ce paramètre.

Les appels dépendants de cas ici, alors, sont ceux à set_hparams() et set_opt_hparams(). Ici,

  • set_hparams() apparaît parce que, dans la définition du modèle, nous avions initialize() prendre un paramètre, output_size. Tous les arguments attendus par initialize() doivent être passés via cette méthode.

  • set_opt_hparams() est là parce que nous voulons utiliser un taux d’apprentissage non par défaut avec optim_adam(). Si nous nous contentions de la valeur par défaut, aucun appel de ce type ne serait de mise.

fitted <- net %>%
  setup(
    loss = nn_bce_with_logits_loss(),
    optimizer = optim_adam,
    metrics = list(
      luz_metric_binary_accuracy_with_logits()
    )
  ) %>%
  set_hparams(output_size = 1) %>%
  set_opt_hparams(lr = 0.01) %>%
  fit(train_dl, epochs = 3, valid_data = valid_dl)

Voici comment la sortie m’a semblé:

predict(fitted, test_dl)

probs <- torch_sigmoid(preds)
print(probs, n = 5)
torch_tensor
 1.2959e-01
 1.3032e-03
 6.1966e-05
 5.9575e-01
 4.5577e-03
... [the output was truncated (use n=-1 to disable)]
[ CPUFloatType{5000} ]

Et c’est tout pour un flux de travail complet. Si vous avez une expérience préalable avec Keras, cela devrait vous sembler assez familier. La même chose peut être dite pour la technique de personnalisation la plus polyvalente mais standardisée mise en œuvre dans luz.

Comment faire (presque) n’importe quoi (presque) n’importe quand

Comme dur, luz a le concept de rappels qui peut « s’accrocher » au processus de formation et exécuter du code R arbitraire. Plus précisément, le code peut être programmé pour s’exécuter à l’un des moments suivants :

  • lorsque le processus global de formation commence ou se termine (on_fit_begin() / on_fit_end());

  • quand commence ou se termine une période de formation plus validation (on_epoch_begin() / on_epoch_end());

  • quand au cours d’une époque, la moitié de la formation (validation, resp.) commence ou se termine (on_train_begin() / on_train_end(); on_valid_begin() / on_valid_end());

  • lorsque pendant la formation (validation, resp.) un nouveau lot est soit sur le point d’être traité, soit a été traité (on_train_batch_begin() / on_train_batch_end(); on_valid_batch_begin() / on_valid_batch_end());

  • et même à des points de repère spécifiques à l’intérieur de la logique de formation/validation « la plus profonde », comme « après le calcul de la perte », « après le retour en arrière » ou « après l’étape ».

Bien que vous puissiez implémenter la logique que vous souhaitez en utilisant cette technique, luz est déjà équipé d’un ensemble très utile de rappels.

Par exemple:

  • luz_callback_model_checkpoint() enregistre périodiquement les poids du modèle.

  • luz_callback_lr_scheduler() permet d’activer l’un des torchc’est ordonnanceurs de taux d’apprentissage. Différents planificateurs existent, chacun suivant sa propre logique dans la façon dont il ajuste dynamiquement le taux d’apprentissage.

  • luz_callback_early_stopping() met fin à la formation une fois que les performances du modèle ne s’améliorent plus.

Les rappels sont transmis à fit() dans une liste. Ici, nous adaptons notre exemple ci-dessus, en veillant à ce que (1) les poids du modèle soient enregistrés après chaque époque et (2), la formation se termine si la perte de validation ne s’améliore pas pendant deux époques consécutives.

fitted <- net %>%
  setup(
    loss = nn_bce_with_logits_loss(),
    optimizer = optim_adam,
    metrics = list(
      luz_metric_binary_accuracy_with_logits()
    )
  ) %>%
  set_hparams(output_size = 1) %>%
  set_opt_hparams(lr = 0.01) %>%
  fit(train_dl,
      epochs = 10,
      valid_data = valid_dl,
      callbacks = list(luz_callback_model_checkpoint(path = "./models"),
                       luz_callback_early_stopping(patience = 2)))

Qu’en est-il des autres types d’exigences de flexibilité – comme dans le scénario de plusieurs modèles interactifs, équipés chacun de leurs propres fonctions de perte et optimiseurs ? Dans de tels cas, le code deviendra un peu plus long que ce que nous avons vu ici, mais luz peut encore aider considérablement à rationaliser le flux de travail.

Pour conclure, en utilisant luzvous ne perdez rien de la flexibilité qui vient avec torch, tout en gagnant beaucoup en simplicité de code, en modularité et en maintenabilité. Nous serions heureux d’apprendre que vous l’essayerez !

Merci d’avoir lu!

photo par JD Rincs sur Unsplash

Krijevsky, Alex. 2014. « Une astuce étrange pour paralléliser les réseaux de neurones convolutifs. » CoRR abs/1404.5997. http://arxiv.org/abs/1404.5997.