Mon code Python est un réseau de neurones
(blog.gabornyeki.com)- 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
identifier–open_paren–close_parenen 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, plusStateet 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()FTPSACKdebug_error()NULLIS_ERROR()aarch64,amd64if (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 manqueif (err) goto cleanup; - La règle qui considère qu’un mot entièrement en majuscules est du code détecte
FTPSACKetIS_ERROR(), mais produit des faux positifs sur des abréviations commeAFAICT - 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
- La règle qui considère qu’un mot suivi de parenthèses est du code détecte des cas comme
- 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_identifier–open_paren–close_parencomme une référence au code - L’implémentation Python stocke l’état des tokens précédents dans une dataclass
Stateprevious_was_identifierprevious_was_open_parenprevious_previous_was_identifierseen_code
contains_codeparcourt les tokens, appelleprocess, puis retourne à la finstate.seen_codeprocessdéfinitseen_codeàTruesi le token courant estclose_paren, que le précédent estopen_parenet 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
Stateainsi que les branchesif/elif/elseaugmentent, et plus on affine les règles, plus la maintenance devient difficile
Transformer la machine à états en RNN
contains_codeetprocessforment 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 à
Stateen Python sont représentées par l’état caché du RNNState_0est l’état initial- Chaque
State_test calculé en appliquant la fonctionfau token courant et à l’état précédent - L’état final passe par la couche de sortie
gpour 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_parenetclose_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
treçoit en entrée la troisième couche du tokent-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
- Dans l’architecture d’exemple, la première couche du token
- 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
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.
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.
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é.
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...
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.
Fait intéressant, ce théorème concerne les réseaux à une seule couche. En pratique, les réseaux à plusieurs couches fonctionnent bien mieux.
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.
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.
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.
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.
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
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.
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.
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.