GPT implémenté en 500 lignes de SQL
(explainextended.com)- Reproduit le flux d’inférence de GPT-2 en SQL uniquement avec PostgreSQL et pgvector, de la tokenisation aux blocs Transformer jusqu’à la génération du token suivant
- Un LLM génératif se rapproche d’une fonction déterministe qui produit, pour une même entrée, les mêmes probabilités sur les tokens candidats ; la variabilité de la réponse intervient à l’étape probabiliste de sélection du token
- L’implémentation exprime en requêtes SQL et en tables le tokenizer BPE de GPT-2, ses 50 257 tokens, ses embeddings de dimension 768, son contexte de 1 024 tokens, ses 12 blocs, ses 12 têtes d’attention et son feedforward basé sur GELU
- Des contournements pragmatiques ont été nécessaires pour s’adapter à l’environnement base de données, comme l’absence de prise en charge des propriétés Unicode dans les regex PostgreSQL ou les limites de
EXPsur les très petites valeurs - L’exemple génère 10 tokens à partir de
"Happy New Year! I wish you"et produit"Happy New Year! I wish you all the best in your new year!", en 2 min 44 s sur l’environnement de l’auteur
Construire un pipeline d’inférence GPT-2 en SQL
- ChatGPT avait répondu que SQL n’était pas adapté à l’implémentation d’un grand modèle de langage, mais l’auteur a pourtant réalisé un pipeline d’inférence GPT-2 en SQL PostgreSQL
- L’implémentation de référence est celle décrite par Jay Mody dans GPT in 60 Lines of NumPy, dont les mêmes composants sont transposés en tables et requêtes de base de données
- Un LLM génératif peut être vu comme une fonction du type
llm(prompt: str) -> list[tuple[str, float]]- l’entrée est un prompt textuel
- la sortie est une liste de chaînes candidates suivantes avec leurs probabilités
- à paramètres et calculs internes identiques, la même entrée renvoie le même résultat
- Si des produits comme ChatGPT peuvent donner des réponses différentes à une même question, c’est moins à cause du modèle lui-même que de la sélection probabiliste du token suivant
Boucle de génération de texte
- Le processus de génération convertit d’abord le prompt en tableau de tokens, puis appelle le modèle en boucle pour choisir le token suivant et l’ajouter au prompt
- Le flux de base se compose des étapes suivantes
tokenize(prompt)convertit la chaîne en tableau d’identifiants de tokensgpt2(tokens)calcule les probabilités sur les 50 257 tokensselect_next_token(candidates)choisit le token suivant- le token choisi est ajouté au tableau
- l’exécution s’arrête selon des conditions comme le nombre de tokens, un timeout ou un stopword
detokenize(tokens)reconvertit le tableau de tokens en chaîne
- La séquence de tokens ainsi accumulée peut devenir un texte en langage naturel présentant des propriétés qui ressemblent à de la grammaire, de la syntaxe, de la sémantique et du raisonnement
Implémenter le tokenizer BPE en SQL
- Avant d’entrer dans le réseau de neurones, le texte doit être transformé en liste de nombres, mais utiliser directement les points de code Unicode rendrait l’espace de tokens et la longueur inefficaces
- GPT-2 utilise une variante du Byte pair encoding
- le vocabulaire de tokens utilise 50 257 points de code
- il inclut les séquences d’octets UTF-8 et le token « end of text »
- au départ, on part de 256 tokens d’octets, puis on ajoute comme nouveaux tokens les paires adjacentes fréquentes
- cette fusion est répétée 50 000 fois pour obtenir 50 256 tokens, puis on ajoute le token de fin de texte
- Le tokenizer GPT-2 comprend une couche supplémentaire qui mappe les octets vers des caractères de chaîne, définie dans
encoder.pyd’OpenAI GPT-2 - Dans l’implémentation SQL, le vocabulaire téléchargé depuis OpenAI est inséré dans la table
tokenizer, et la tableencoderstocke le mapping octet-caractère - L’exemple
"Mississippilessly"part d’octets simples via un CTE récursif, puis fusionne à répétition la meilleure paire adjacente possible- dans cet exemple, le nombre de tokens passe de 17 à 5
- on utilise l’espace d’environ 50k tokens de GPT-2 au lieu d’un espace d’environ 150k points de code Unicode
- Pour traiter plusieurs mots, GPT-2 segmente le texte avec une regex puis effectue les fusions à l’intérieur de chaque mot
- PostgreSQL ne prend pas en charge les propriétés de caractères Unicode dans les regex, donc la regex GPT-2 d’origine a dû être partiellement modifiée
- cette modification a probablement dégradé la prise en charge correcte d’Unicode
"PostgreSQL is great"est converti par le tokenizer SQL en[6307, 47701, 318, 1049]- les groupes de tokens sont
Post,greSQL,Ġis,Ġgreat Ġreprésente un espace
- les groupes de tokens sont
Embeddings et fenêtre de contexte
- Les identifiants de tokens ne sont pas utilisés directement dans les calculs du modèle : ils sont convertis en vecteurs d’embedding
- GPT-2 encode séparément les tokens et leurs positions
WTEest l’embedding des tokens et correspond à une matrice 50257×768WPEest l’embedding de position et correspond à une matrice 1024×768
- Pour chaque position de token, on additionne le vecteur
WTEet le vecteurWPEpour produire le vecteur d’entrée de l’étape suivante - Comme
WPEne contient que 1 024 positions, le nombre maximal de tokens utilisables dans un prompt GPT-2 est de 1 024- cela correspond à la fenêtre de contexte du LLM
- c’est un hyperparamètre fixé à la conception du modèle, et non appris
- L’implémentation SQL utilise pgvector
- il serait possible de définir directement des opérations vectorielles sur des tableaux en SQL pur, mais avec de moins bonnes performances
- une première version fonctionnait avec des fonctions SQL pures, mais était lente
Dérouler le self-attention en requêtes SQL
- Le cœur du Transformer est le mécanisme de self-attention, issu de l’article de 2017 Attention is all you need
- L’attention permet aux vecteurs de tokens de s’influencer mutuellement, afin que l’information du début du prompt soit transmise jusqu’au dernier vecteur
- L’implémentation GPT-2 utilise 12 ensembles de matrices
Q,K,V- chaque ensemble constitue une tête d’attention
- chaque tête est de dimension 64
c_attnest une transformation linéaire 768×2304, dont le résultat est un vecteur de dimension 2304 oùQ,KetVsont concaténés horizontalement- les poids et biais sont stockés dans les tables
c_attn_wetc_attn_b
- Avant le calcul d’attention, une normalisation de couche est appliquée
- les paramètres d’échelle et de décalage sont stockés dans les tables
ln_1_getln_1_b
- les paramètres d’échelle et de décalage sont stockés dans les tables
- En causal self-attention, on applique un masque causal pour empêcher les tokens suivants d’influencer les tokens précédents
- les candidats du token suivant sont déterminés au final à partir du dernier embedding
- le flux d’information doit aller vers le dernier vecteur, sans que les valeurs intermédiaires du dernier vecteur influencent les vecteurs précédents
- L’implémentation SQL traite à 0 les entrées inférieures à -745.13 dans le calcul du softmax afin d’éviter les problèmes de PostgreSQL
EXPsur les très petites valeurs - Grâce au masque causal, l’ajout d’un nouveau token au prompt ne modifie pas les résultats déjà calculés pour les tokens précédents
- l’implémentation GPT-2 d’origine exploite cette propriété
- l’implémentation SQL ne réutilise pas ces calculs, par souci de simplicité
Multi-head attention et connexions résiduelles
- Les résultats d’attention des 12 têtes font chacun 64 dimensions ; ils sont concaténés horizontalement pour reformer 768 dimensions
- On projette ensuite la sortie de l’attention à l’aide de la transformation linéaire apprise stockée dans
c_proj_wetc_proj_b - Le résultat du multi-head attention se voit réadditionner l’entrée d’origine
- cette connexion résiduelle fait partie du Transformer d’origine
- elle a été introduite pour atténuer les problèmes de vanishing et exploding gradients pendant l’apprentissage
Étape feedforward et blocs Transformer
- Après l’attention vient un réseau feedforward
- Dans GPT-2, l’étape feedforward est constituée d’un perceptron multicouche à 3 couches
- dimensions : 768 → 3072 → 768
- la fonction d’activation utilisée est GELU
- Les paramètres des transformations linéaires sont stockés dans les tables suivantes
mlp_c_fc_w,mlp_c_fc_bmlp_c_proj_w,mlp_c_proj_b
- L’entrée du feedforward est elle aussi normalisée d’abord avec les paramètres
ln_2ln_2_getln_2_bstockent l’échelle et le décalage
- Une connexion résiduelle réajoute également l’entrée à la sortie du feedforward
- Cet ensemble attention + feedforward constitue un bloc, et GPT-2 en chaîne 12 comme un pipeline
- chaque bloc possède son propre jeu de paramètres appris
- en SQL, les blocs sont chaînés via un CTE récursif
- La sortie du dernier bloc est de nouveau normalisée avec les paramètres
ln_f
Revenir au token suivant
- Dans la sortie finale, le vecteur de dimension 768 de la dernière position correspond à l’embedding du token suivant
- Pour reconvertir ce vecteur en token, on réutilise la matrice
WTEdéjà employée pour l’embedding initial des tokens - Une transformation inverse exacte est généralement impossible
- l’embedding prédit peut ne correspondre exactement à aucune ligne spécifique de
WTE - on calcule donc le produit scalaire avec chaque embedding de token pour trouver les tokens les plus proches
- l’embedding prédit peut ne correspondre exactement à aucune ligne spécifique de
- Le produit scalaire entre
WTEet l’embedding prédit donne 50 257 scores, c’est-à-dire des logits - Ces scores sont ensuite transformés en probabilités via un softmax
- le nombre de meilleurs candidats est
top_n - la valeur qui module la distribution de probabilité est la temperature
- plus la temperature est élevée, plus des tokens autres que le premier ont des chances d’être choisis, et moins l’inférence est prévisible
- le nombre de meilleurs candidats est
- Dans l’exemple
"PostgreSQL is great", les 5 meilleurs candidats pour le token suivant sontĠfor,.ĠatĠto
- Quand la temperature passe à 0.5, 1 puis 2, la distribution de probabilité softmax des mêmes candidats change
Résultats d’inférence réels et code
- Le SQL final répète le processus qui sélectionne un token selon sa probabilité puis l’ajoute au prompt
- Le modèle lui-même est déterministe ; le seul élément non déterministe est le hasard inclus dans la sélection du token
- La configuration de l’exemple est la suivante
- prompt :
"Happy New Year! I wish you" - nombre de tokens générés : 10
- temperature : 2
- top_n : 1
- utilisation de
SETSEED(0.20231231)
- prompt :
- Dans l’environnement de l’auteur, la requête s’exécute en 2 min 44 s
- Le résultat produit est
"Happy New Year! I wish you all the best in your new year!" - La requête et le code d’installation sont disponibles dans le dépôt GitHub quassnoi/explain-extended-2024
1 commentaires
Avis sur Hacker News
Magnifique. Je creusais moi aussi un terrier similaire avec SQLite, mais je n’en étais pas encore arrivé à y faire entrer des réseaux de neurones.
J’avais été inspiré par la série de cours makemore[0] ; au bout d’environ une heure, on passe d’une approche par comptage à un réseau de neurones, et c’est à peu près là où j’étais arrivé.
Le décomposer selon le modèle relationnel est vraiment un excellent exercice.
[0] https://www.youtube.com/watch?v=PaCmpygFfXo
Bonne démo, mais l’explication du masquage causal dans l’article semble mélanger apprentissage et inférence.
Le masquage causal sert à empêcher le modèle de « regarder » les tokens futurs pendant l’apprentissage, et dans les architectures de type GPT, il sert aussi à imposer l’autorégressivité pendant l’inférence.
Lors de l’inférence, de toute façon, seul le dernier token est utilisé ; ce token prête donc attention à toute la séquence d’entrée, et le token suivant n’est donc pas déterminé uniquement par l’embedding du dernier token.
Je me demande si cela représente correctement la boucle de pilotage de GPT : on tokenise le prompt, on obtient avec
gpt2(tokens)les probabilités des 50 257 tokens, on choisit le token suivant, on l’ajoute à la liste de tokens, on vérifie les conditions d’arrêt, puis on détokenise à la fin.Mais cela donne l’impression qu’une machine à états implémente l’algorithme de Shlemiel the painter, ce qui soulève des questions sur le coût de calcul intrinsèque de la génération.
Cette fenêtre est une fenêtre glissante.
gpt2.Cela montre surtout comment rassembler les tokens générés en une phrase.
Ressource liée : A GPT in 60 Lines of NumPy - https://news.ycombinator.com/item?id=34726115 - février 2023, 146 commentaires
Dans le même esprit, j’ai implémenté entièrement GPT avec des fonctions de tableur, et créé aussi un tutoriel vidéo à regarder en parallèle.
https://spreadsheets-are-all-you-need.ai/
Je trouve les LLM assez fascinants, mais je n’avais pas besoin d’apprendre professionnellement leur fonctionnement réel ; en 10 minutes, cette vidéo m’en a appris plus que plusieurs années de commentaires HN obscurs et d’articles superficiels dans les médias grand public.
Voir ces quantités énormes de nombres en virgule flottante empilés en attente de calcul rend aussi beaucoup plus intuitif pourquoi cette technologie consomme autant de GPU.
En calculant, pour chaque exemple d’apprentissage, la dérivée de chaque paramètre, puis en montrant explicitement comment elle se rattache à ce paramètre, on pourrait aussi bien expliquer le processus d’apprentissage.
Très bien. Il y a encore un an, cela ressemblait à une sorte de magie ; désormais, c’est expliqué si clairement que presque un enfant pourrait suivre.
Le modèle expliqué dans l’article est GPT-2, publié début 2019.
Pour bien comprendre cet article, il faut de solides connaissances en informatique, et le titre lui-même reste peu accessible à 99 % de l’humanité.
J’ai complètement évité GPT et les LLM jusqu’ici, mais cette approche semble capable de produire une certaine fluidité en sortie texte, sans pour autant avoir la capacité d’interpréter des questions et d’y répondre.
Je me demande s’il existe un simple billet de blog ou un cours qui explique comment cela fonctionne réellement, ou qui montre un moteur jouet en Python, par exemple.
Les ressources pédagogiques que j’ai vues jusqu’ici se concentrent sur l’utilisation des plateformes, et abordent peu le fonctionnement interne.
En particulier [0], [1] et [2] sont très bons.
[0] http://jalammar.github.io/illustrated-transformer/
[1] http://jalammar.github.io/illustrated-gpt2/
[2] https://jalammar.github.io/visualizing-neural-machine-transl...
Fait intéressant, le machine learning moderne n’exige pas la complétude de Turing.
Et pourtant on envisage la possibilité d’une AGI ; si l’on en venait à conclure que la complétude de Turing n’est pas nécessaire, ce serait assez amusant.
Pour une raison simple : je peux suivre mentalement l’exécution de code Turing-complet.
L’article était excellent, et l’explication de chaque composant était claire et assez approfondie, ce qui le rendait agréable à lire.
Mais après avoir cliqué par erreur sur « + expand source » et vu ce monstre stupéfiant, je comprends mieux pourquoi ChatGPT dit que « SQL n’est pas adapté à l’implémentation de grands modèles de langage ».
Dire que « l’Unicode ordinaire ne se marie pas bien avec les réseaux de neurones » n’est pas vrai. Il suffit de regarder ByT5, par exemple.
Ce que l’article appelle « alphabet » est généralement appelé vocabulaire, et si l’on utilise les octets UTF-8 comme vocabulaire, on obtient 256 tokens, pas 149 186.
C’est exactement ce que fait ByT5.
Le fait que tous les modèles les plus performants du marché utilisent la tokenisation en est la preuve.
Ce n’est un secret pour personne que la tokenisation est fondamentalement une sorte de hack, et qu’idéalement on aimerait s’en débarrasser un jour d’une manière ou d’une autre (https://twitter.com/karpathy/status/1657949234535211009).
En principe, on peut compenser les défauts de la tokenisation au niveau des octets avec des modèles plus grands et des contextes plus longs, mais en pratique il faut beaucoup plus de ressources pour entraîner un modèle de niveau d’intelligence équivalent.
Bien sûr, il existe aussi des tâches particulières, comme compter le nombre de lettres dans un mot, où la tokenisation nuit au contraire à l’intelligence.