2 points par GN⁺ 2024-07-02 | 1 commentaires | Partager sur WhatsApp
  • Les programmes d’extraction d’informations ambiguës ont facilement tendance à devenir du spaghetti code à mesure que les règles d’exception s’accumulent, et cette logique basée sur l’état peut être repensée comme un réseau de neurones récurrent (RNN)
  • L’exemple de recherche de références au code C dans des messages de revue de code est implémenté comme un classificateur écrit à la main qui suit des motifs de tokens comme identifieropen_parenclose_paren en tant qu’états
  • Cette règle affiche une précision de 100 % dans l’exemple, mais manque des cas comme if (err) goto cleanup;, ce qui limite le rappel à 50 %, et plus on ajoute de règles, plus State et les branchements deviennent complexes
  • La même machine à états peut être encodée avec l’état caché d’un RNN et les calculs de couches, et pour la rendre entraînable, il faut remplacer les fonctions indicatrices binaires par ReLU, sigmoid, ainsi que des poids et biais entraînables
  • Les implémentations comme l’Elman RNN, le GRU et le LSTM de PyTorch, ainsi que le problème du vanishing gradient sur les longues séquences de tokens, constituent de vraies contraintes pour l’entraînement, et le fait même de définir le dataset, les labels et la fonction de perte aide aussi à concevoir des règles manuelles

Comment l’extraction de données ambiguës devient du spaghetti code

  • Dans les programmes de recherche conçus pour extraire des informations à partir de données brutes, les règles deviennent vite complexes lorsque les données ne suivent pas une spécification claire ou présentent des formes atypiques
  • Parmi les tâches d’exemple figurent l’identification d’entreprises et de dirigeants dans des articles de presse, l’étiquetage de contrats publics d’approvisionnement par type de service, ou la détection de la présence de code dans des messages d’ingénieurs
  • Si l’on veut une sortie parfaite, on peut examiner attentivement chaque observation et écrire des tests unitaires pour des cas représentatifs
    • R comme Python fournissent des bibliothèques de test pour cela
  • Dans les situations qui demandent des règles de décision complexes, comme les surnoms, les synonymes, ou la frontière entre anglais et code, les règles écrites à la main deviennent facilement fragiles
  • Les algorithmes d’entraînement de réseaux de neurones transforment ce problème en une recherche de ces combinaisons de règles dans les données, au lieu de demander à un humain de les retoucher sans cesse

Trouver des références au code dans des messages de revue de code

  • L’objectif est de détecter si un message envoyé pendant une revue de code fait explicitement référence à du code informatique
  • On suppose que le code observé est écrit en C
  • Les messages représentatifs contiennent des références au code comme :
    • render_ipa_alloc()
    • FTPSACK
    • debug_error()
    • NULL
    • IS_ERROR()
    • aarch64, amd64
    • if (err) goto cleanup;
  • De simples règles candidates échouent chacune d’une manière différente
    • La règle qui considère qu’un mot suivi de parenthèses est du code détecte des cas comme render_ipa_alloc(), mais manque if (err) goto cleanup;
    • La règle qui considère qu’un mot entièrement en majuscules est du code détecte FTPSACK et IS_ERROR(), mais produit des faux positifs sur des abréviations comme AFAICT
    • La règle qui considère qu’un mot non anglais est du code peut aussi produire de faux positifs sur du vocabulaire d’ingénierie ou des noms d’architecture
  • Pour améliorer les règles 2 et 3, il faut une liste d’abréviations et de termes spécialisés comme AFAICT, LGTM, USD, COVID, aarch64, amd64

Un classificateur construit comme une machine à états écrite à la main

  • Un algorithme simple détermine en deux étapes si un message contient du code
    • Prétraitement : convertir le message en une séquence de tokens reflétant les éléments syntaxiques du code C
    • Inférence : vérifier si la séquence de tokens satisfait les règles
  • La règle 1 considère le motif underscore_identifieropen_parenclose_paren comme une référence au code
  • L’implémentation Python stocke l’état des tokens précédents dans une dataclass State
    • previous_was_identifier
    • previous_was_open_paren
    • previous_previous_was_identifier
    • seen_code
  • contains_code parcourt les tokens, appelle process, puis retourne à la fin state.seen_code
  • process définit seen_code à True si le token courant est close_paren, que le précédent est open_paren et que celui d’avant est un identifiant
  • Ce classificateur ne produit aucun faux positif et une précision de 100 % dans l’exemple, mais comme il manque beaucoup de cas, son rappel reste à 50 %
  • Si l’on ajoute la règle 2, le nombre de champs dans State ainsi que les branches if/elif/else augmentent, et plus on affine les règles, plus la maintenance devient difficile

Transformer la machine à états en RNN

  • contains_code et process forment une machine à états, et une machine à états peut être encodée dans un réseau de neurones récurrent (RNN)
  • Un RNN traite la séquence de tokens un par un et approxime la probabilité conditionnelle qu’un message contienne du code
  • Les valeurs correspondant à State en Python sont représentées par l’état caché du RNN
    • State_0 est l’état initial
    • Chaque State_t est calculé en appliquant la fonction f au token courant et à l’état précédent
    • L’état final passe par la couche de sortie g pour produire le résultat de classification
  • Le RNN d’exemple utilise trois couches cachées
    • La première couche stocke ou recopie le token courant et l’état précédent
    • La deuxième couche vérifie le motif correspondant à la règle 1
    • La troisième couche mémorise si un motif de code a déjà été observé
  • Les tokens sont représentés sous forme de vecteurs binaires en one-hot
  • Pour imiter à l’identique l’algorithme écrit à la main, on peut utiliser la fonction indicatrice binaire 1{x > 0}
    • Les couches cachées peuvent ainsi rester binaires
    • Mais comme sa dérivée vaut 0 presque partout, elle ne convient pas à l’entraînement
  • On peut aussi vérifier le motif avec le produit de identifier, open_paren et close_paren, mais avec des couches cachées binaires, on peut exprimer le même test par une somme
  • Giles et al. (1992) s’y rattachent comme exemple d’utilisation d’un second-order RNN pour découvrir une machine à états

Le transformer en réseau entraînable

  • Pour permettre l’entraînement, on utilise ReLU à la place de la fonction indicatrice binaire
  • Les constantes numériques sont remplacées par des poids et biais estimés par descente de gradient
  • La couche de sortie calcule la probabilité finale avec une fonction d’activation sigmoid
  • Cette forme peut être entraînée dans PyTorch, mais telle quelle, elle n’offre pas des performances exceptionnelles
  • L’une des raisons de ces performances limitées est que l’architecture n’est pas standard, donc une plus grande partie de la procédure d’entraînement s’exécute dans le code de liaison Python et exploite moins les implémentations de bibliothèque C++ de PyTorch

Implémentation PyTorch et limites des longs messages

  • torch.nn.RNN de PyTorch fournit une implémentation fondée sur l’Elman RNN
  • L’architecture d’exemple et l’Elman RNN diffèrent dans la manière de connecter les couches cachées
    • Dans l’architecture d’exemple, la première couche du token t reçoit en entrée la troisième couche du token t-1, et chaque couche ne reçoit en entrée que la couche immédiatement précédente
    • Dans l’Elman RNN, chaque couche cachée reçoit aussi l’état de la même couche à l’instant précédent
    • La première couche cachée de l’Elman RNN ne reçoit pas en entrée la couche finale de l’instant précédent
  • Les vrais messages de revue de code peuvent être longs, ce qui conduit à de longues séquences de tokens
  • Sur de longues séquences, même si la descente de gradient fonctionne en théorie, des problèmes de stabilité numérique peuvent apparaître à cause du vanishing gradient
  • L’Elman RNN peut lui aussi être vulnérable à ce problème, et un GRU ou un LSTM peut obtenir de meilleures performances sur la détection de code

Une discipline fondée sur les données

  • Les RNN transforment des combinaisons de règles difficiles à manipuler à la main en objet d’apprentissage, et forcent à définir le problème plus clairement
  • Pour entraîner le réseau, il faut notamment :
    • choisir un dataset d’entraînement et un dataset de validation
    • effectuer un étiquetage préalable
    • définir une fonction de perte qui explicite ce que le classificateur doit atteindre et ce qu’il doit éviter
  • Ce processus révèle des zones grises inattendues et rend les critères de jugement plus explicites
  • Cette discipline fondée sur les données est utile aussi pour des problèmes résolus non par des réseaux de neurones, mais par des algorithmes écrits à la main

1 commentaires

 
GN⁺ 2024-07-02
Avis sur Hacker News
  • L’article parle peu des tests ou de l’obtention de données d’entraînement, alors que cela semble être le point essentiel.
    Quand on a l’impression de comprendre du code, c’est parce qu’on a, au moins informellement, démontré soi-même qu’il se généralise à toutes les entrées. Par exemple, un algorithme de tri trie n’importe quelle liste, pas seulement celles qui ont été testées.
    Ce qui est incertain avec les réseaux de neurones, c’est qu’on ne sait pas comment ils vont généraliser. Si une entrée jamais vue diffère ne serait-ce qu’un peu, il n’y a pas de propriété garantie ; et il peut même s’agir d’un problème dont la propriété souhaitée est difficile à spécifier mathématiquement dès le départ.
    Si l’on peut définir une propriété assez clairement pour l’utiliser dans des tests basés sur les propriétés à la QuickCheck, on peut produire, par randomisation, de grands volumes de tests ou de données d’entraînement. Il suffit d’écrire des tests qui partent d’un exemple souhaité et génèrent des variations possibles d’exemples positifs et négatifs.
    Ce n’est pas une preuve, mais c’est un point de départ. Au moins, si l’on peut prouver quelque chose, on sait quoi prouver.
    Avec ce genre de mécanisme, s’appuyer sur du code spaghetti et s’appuyer sur un réseau de neurones semblent assez proches. Si l’on veut aussi satisfaire d’autres propriétés, il suffit d’ajouter un test basé sur les propriétés. Un réseau de neurones peut être entraîné plutôt que modifié directement, mais il existe aussi de l’assistance IA pour modifier du code.
    Malgré tout, je pense que je ferais davantage confiance au code. Au moins, le débogage est possible.

  • C’est un article intéressant si on le lit comme une méthode pour créer un réseau de neurones qui accomplit une tâche pratique. Mais si, la prochaine fois que vous devez parser une entrée, vous comptez suivre cette approche telle quelle, honnêtement je ne sais pas quoi dire.
    L’auteur prend un problème difficile — parser une entrée arbitraire avec des motifs définis de façon lâche — et dit, à juste titre, que cela risque fortement de produire du code spaghetti difficile à lire.
    Mais comme alternative, il propose du code si difficile à lire qu’on en étudie encore le fonctionnement : un réseau de neurones.
    Je comprends l’idée, mais il ne faut pas surestimer quelque chose de totalement ininterprétable par rapport à quelque chose de « moche ». Pour certaines tâches, un modèle de machine learning peut convenir, mais dans beaucoup de cas, même si cela demande des efforts, il vaut mieux pouvoir lire et vérifier pourquoi quelque chose fonctionne ainsi plutôt que ne pas pouvoir le faire.

    • Je pense que le recours au code spaghetti par l’auteur relève presque du détournement du sujet. Quand la sortie de l’algorithme n’est pas définie précisément comme une fonction de l’entrée, mais qu’on dispose d’exemples à montrer, c’est là que le machine learning est utile.
      Au final, le machine learning ne fait qu’ajouter une option. Le fait qu’elle convienne dépend des résultats d’évaluation, ainsi que du niveau de déterminisme et d’explicabilité requis par l’algorithme choisi.
      Ce qui ressort, c’est la question de savoir si un RNN est le bon choix. Il faut l’entraîner, et il pourrait nécessiter beaucoup plus d’exemples que ceux disponibles. Cela dit, il semble possible de créer des données synthétiques de cas positifs et négatifs à partir des règles connues.
    • L’approche par code spaghetti est fondamentalement un système expert. On peut y voir une IA algorithmique à l’ancienne. En dehors de domaines restreints, ces systèmes ont rarement bien fonctionné, et la réalité est trop désordonnée.
      Un système dans lequel on peut voir pourquoi il se comporte ainsi est appréciable, mais cela n’a pas de sens s’il donne constamment de mauvaises réponses. En usage réel, obtenir la bonne réponse est souvent plus important que savoir comment on y est arrivé.
    • On dirait qu’il s’agit de cacher du code spaghetti disgracieux derrière une jolie matrice de nombres à virgule flottante 1000x1000.
  • Il existe, pour les réseaux de neurones, le théorème d’approximation universelle. Il affirme qu’ils peuvent représenter ou encoder n’importe quelle fonction jusqu’au niveau de précision souhaité[0].
    Mais il n’existe pas de théorème disant qu’on peut apprendre une telle approximation, ni comment l’apprendre.
    [0] https://en.m.wikipedia.org/wiki/Universal_approximation_theo...

    • Cette preuve est souvent invoquée, mais ce qu’elle montre en pratique, c’est surtout qu’un réseau de neurones est équivalent à une table de consultation. Une table de consultation disposant de suffisamment de mémoire peut approximer n’importe quelle fonction.
      On est loin d’une explication du fonctionnement réel des réseaux de neurones pratiques et utiles comme les réseaux convolutionnels, les transformers ou les LSTM.
    • À noter qu’il existe de nombreux algorithmes dont on a prouvé, bien avant les réseaux de neurones, qu’ils étaient des approximateurs universels de fonctions. Les réseaux de neurones ne sont ni les seuls ni les premiers. Dans bien des cas, il existe même des méthodes nettement plus adaptées qu’eux.
    • Il ne s’agit pas de n’importe quelle fonction. Il existe des contraintes sur les types de fonctions auxquels s’applique le théorème d’approximation universelle.
      Fait intéressant, ce théorème concerne les réseaux à une seule couche. En pratique, les réseaux à plusieurs couches fonctionnent bien mieux.
    • Ils ne peuvent modéliser que des fonctions continues ; plus précisément, toute fonction continue définie sur un sous-ensemble compact de ℝⁿ peut être approximée avec une précision arbitraire dès lors qu’il y a suffisamment de neurones.
    • Dans ce cas, je me demande ce que signifie l’apprentissage.
  • C’est un très bon article, et même si je ne comprends pas complètement les concepts mathématiques plus profonds autour des RNN, il suscite beaucoup de réflexions.
    Cela ressemble à ce que j’explore ces derniers temps : une façon de construire des apps en les reliant à un algorithme d’inférence avant. L’auteur utilise un RNN ; de mon côté, je construis quelque chose autour de l’algorithme de Rete.
    Je trouve aussi très puissante l’idée de considérer que l’on consomme une chaîne d’entrée caractère par caractère. Ainsi, on confie la logique d’inférence à l’algorithme, on n’écrit qu’une couche très fine d’entrées-sorties, et l’algorithme s’occupe du reste.

  • Ce qui rend cet article intéressant, c’est qu’il explique concrètement ce que signifie transformer une fonction en RNN, puis met cela en regard d’un RNN « batteries incluses » fourni par PyTorch, sous forme de retour d’expérience d’apprentissage.
    Ma question est la suivante : l’article dit qu’il faut ajouter trois couches cachées au réseau pour modéliser l’état ; pourquoi trois ? Est-ce une conséquence des règles particulières qu’il voulait implémenter, ou bien est-ce le nombre de couches généralement utilisé pour implémenter ce type de règles avec cette architecture ? Je me demande aussi si une structure d’Elman aurait permis de le faire avec moins de couches.

    • Pour répondre à la première question, utiliser trois couches cachées rend un peu plus clair ce que fait le réseau. Chaque couche effectue une étape du calcul.
      La première couche rassemble ce que l’on peut savoir à partir du token courant et ce que l’on savait après le calcul sur le token précédent. La deuxième couche vérifie si la règle de décision est satisfaite pour déterminer si le token courant ressemble à du code de programme. La troisième couche compare cette décision avec celles prises pour les tokens précédents.
      J’ai l’impression que cela pourrait aussi être condensé en une seule couche cachée. Comme ReLU suffit à capturer la non-linéarité, cela semble possible. Je n’ai pas encore suffisamment étudié la correspondance avec la structure d’Elman, donc je ne connais pas la réponse.
  • Les RNN ont-ils été complètement absorbés par les transformeurs ? Je me demande si on peut oublier comment manipuler les RNN et se concentrer uniquement sur les transformeurs.

    • Pour rendre cette question plus complexe, l’article « Transformers are RNNs: Fast Autoregressive Transformers with Linear Attention » vaut le détour - https://arxiv.org/pdf/2006.16236
      Il montre qu’une certaine définition étroite des transformeurs, à savoir les transformeurs avec masquage causal, est équivalente aux RNN, et que l’inverse est également possible.
      De même, Mamba (https://arxiv.org/abs/2312.00752), une architecture très en vue en ce moment, possède aussi une unité équivalente à un RNN avec portes. Si j’ai bien compris, pour des raisons de performance, elle utilise un CNN équivalent à l’entraînement, et un RNN à l’inférence.
    • Les transformeurs ont un contexte fini, contrairement aux RNN. En pratique, le signal de gradient des RNN est limité et s’atténue à cause de la rétropropagation à travers le temps.
      C’est en fait l’un des principaux avantages des transformeurs : les dépendances proches et lointaines ne sont pas plus difficiles ni plus faciles à modéliser. Mais, en théorie, un RNN peut mémoriser un passé infiniment lointain.
    • Si tu veux devenir docteur en machine learning ou chercheur, non ; sinon, oui.
      J’ai travaillé sur le machine learning/les LLM comme ingénieur de recherche ces sept dernières années, y compris dans un labo de recherche d’une FAANG, et je me suis toujours dit que je devrais apprendre les RNN, mais je ne les ai jamais vraiment appris et je n’en ai jamais eu besoin.
  • Si le sujet t’intéresse, ça vaut le coup de regarder la programmation génétique. J’y vois une approche plus simple du même problème, qui ne nécessite pas de maths.
    On recombine les programmes à partir de leur arbre syntaxique abstrait et, si on fournit une heuristique, le programme est optimisé selon ce critère. La magie est dans la fonction heuristique : on peut choisir d’optimiser ce qu’on veut, comme la vitesse, la longueur du programme, la minimisation des structures complexes ou des appels de fonction, l’efficacité réseau, ou une combinaison de tout cela.
    https://youtu.be/tTMpKrKkYXo

    • J’ajouterais aussi les Humies Awards, qui présentent des résultats compétitifs au niveau humain. Rien qu’en parcourant les articles soumis, on peut beaucoup apprendre sur ce qui est possible ou non dans ce domaine.
      https://www.human-competitive.org/
  • J’ai récemment écrit un billet de blog explorant l’idée d’interfacer un LLM local pour ce type de tâches floues.
    Ça me paraît plus logique que de coder directement un réseau de neurones. Il me semble plus pratique d’utiliser quelque chose comme llama.cpp pour évaluer si un petit modèle peut résoudre le problème tel quel, puis, si ce n’est pas le cas, de le fine-tuner avant de l’intégrer par programmation avec llama.cpp via le wrapper souhaité.

  • Les réseaux de neurones récurrents peuvent servir à des calculs arbitraires, et leur équivalence avec les machines de Turing a été prouvée. Mais ils sont totalement irréalistes pour cette tâche.
    L’approche de cet article ressemble plutôt à une sorte de machine à états apprise. L’article gagnerait à inclure un résumé plus long, et « Python » ne semble pas vraiment pertinent. Apprendre la vraie sémantique de Python serait assez difficile, vu les caractéristiques du langage : ce n’est pas un langage défini par un standard, il fonctionne comme CPython fonctionne.

    • L’article de Karpathy de 2015 sur les RNN[1] montrait qu’un RNN entraîné caractère par caractère sur les œuvres de Shakespeare pouvait produire du texte dans le style de Shakespeare, même sans la cohérence narrative d’un LLM.
      Dès lors, pourquoi ne pourrait-il pas traiter du langage naturel formel, comme des commentaires de revue de code ?
      Dans ce cas, l’inférence était lancée avec une entrée aléatoire pour générer du « Shakespeare » aléatoire, mais la structure et le style de la langue avaient tout de même été appris par le RNN. Peut-être que cela pourrait aussi servir à de la classification.
      1. https://karpathy.github.io/2015/05/21/rnn-effectiveness/
  • Il suffit d’abord de compiler Python en réseau de neurones, puis de bricoler ça pour le greffer sur un réseau de neurones basé sur des transformeurs.
    Ainsi, une Transformer Virtual Machine (TVM) pourrait exécuter des programmes arbitraires.
    Avec le transfer learning, c’est-à-dire en superposant les poids, un LLM pourrait « naître » avec des algorithmes profondément encodés.