1 points par GN⁺ 2023-08-15 | 1 commentaires | Partager sur WhatsApp
  • Outlines est un outil de sortie structurée pour les LLM, qui permet de spécifier le type de sortie souhaité pendant la génération afin de garantir que les données correspondent exactement à cette structure
  • Au lieu de corriger après génération des sorties invalides à l’aide de parsing, d’expressions régulières ou de code fragile, il impose directement une structure valide dès l’étape de génération
  • L’utilisation suit la forme model(prompt, output_type) et permet de définir la sortie en cohérence avec le système de types Python, comme Literal["Yes", "No"], int ou un modèle Pydantic
  • Les types de sortie pris en charge incluent des choix prédéfinis, des Function Calls basés sur des signatures de fonction, des schémas JSON/Pydantic, des motifs d’expression régulière et une contrainte de structure fondée sur des grammaires
  • L’intégration des modèles se répartit entre vLLM et Ollama pour les serveurs, transformers et llama.cpp pour les modèles locaux, et OpenAI, Gemini et Dottxt pour les API
  • Les exemples de workflow montrent comment convertir des e-mails clients en tickets de service, transformer des descriptions de produits en données de catégorie, extraire des informations structurées à partir de descriptions d’événements incomplètes ou renvoyer "I don't know", classer des documents dans des catégories prédéfinies, ou convertir des demandes de réunion en langage naturel en paramètres de fonction
  • Les templates de prompt prennent en charge outlines.Template.from_string basé sur Jinja ainsi que le chargement depuis des fichiers, afin de séparer les prompts complexes du code et de les réutiliser
  • L’installation se fait avec pip install outlines, et l’API .txt est actuellement indiquée comme étant en accès anticipé

1 commentaires

 
GN⁺ 2023-08-15
Avis de Hacker News
  • Cette bibliothèque semble reposer, côté mécanisme, sur une idée simple consistant à masquer une partie de l’espace lexical et à avancer efficacement pas à pas dans le temps, et c’est excellent.
    Cela dit, pour avoir utilisé des bibliothèques qui plaquent une structure de sortie sur un LLM de base, je me demande si des modèles de base comme Llama2 fonctionnent vraiment bien.
    D’après mon expérience, on est plutôt proche de « pas du tout », et pour que cela fonctionne réellement, il fallait pas mal de tuning par instructions adapté à un usage précis.
    En plus, masquer a posteriori l’espace d’états pendant la génération avec un modèle déjà tuné par instructions revient au final à modifier la distribution de génération, ce qui semble contre-intuitif puisqu’on pourrait penser que cela nuit au tuning par instructions.

    • Je ne vois pas très bien pourquoi il faudrait absolument utiliser le llama-2 original. Sur le Hub HF, il existe énormément de versions très solides de llama-2 finement ajustées par instructions, et ces modèles feront bien mieux le travail. Par exemple, il y a Beluga-2 de Stability-AI : https://huggingface.co/spaces/HuggingFaceH4/open_llm_leaderb...
      Sur le deuxième point, si l’objectif est par exemple de faire en sorte que le modèle ne génère que du JSON, alors c’est possible à 100 % en limitant quels tokens de sortie peuvent ou ne peuvent pas être utilisés.
    • J’ai été assez impressionné par Llama 2 13B, et plus je l’utilise longtemps, plus je me dis qu’il pourrait être réellement utile au-delà du simple jouet de LLM local.
      Comme je peux utiliser le GPU sur un Mac M2, j’utilise la version MLC via le plugin https://github.com/simonw/llm-mlc.
    • Au moins pour la génération de code, guider la génération au niveau des tokens pouvait nettement améliorer même les modèles de base.
      L’article “Guiding Language Models of Code with Global Context using Monitors” (https://arxiv.org/abs/2306.10763) propose le Monitor Guided Decoding, qui connecte un LLM à de l’analyse statique pour le guider vers la génération de code cohérent du point de vue des types.
      Sans aucun fine-tuning, guider à certains endroits la génération au niveau des tokens grâce à l’analyse statique a fortement amélioré la qualité du code généré, à la fois en termes de compilabilité et de correspondance avec la bonne réponse. Même de tout petits modèles (1,1B) produisaient davantage de code compilable que des modèles beaucoup plus grands (175B), tout en améliorant aussi la correspondance avec la réponse attendue.
    • Si « le masquage a posteriori pendant la génération avec un modèle tuné par instructions modifie la distribution de génération », j’ai l’impression que c’est précisément ce qu’on faisait en développement piloté par les tests.
      La principale différence, c’est seulement que la fonction de génération n’était pas un LLM, mais un humain. Je ne vois pas pourquoi on ne pourrait pas retirer l’humain du milieu.
    • Le tuning par instructions lui-même est plutôt « trivial » ; le vrai problème, c’est de gérer les conditions aux limites.
      Dans le code traditionnel, les conditions aux limites étaient littéralement de petits cas particuliers, mais avec les LLM, on ne sait pas ce qui va les faire partir dans une direction absurde, et le code de parsing doit encaisser ce chaos.
      Autrement dit, on a l’impression que la proportion de cas à traiter comme des conditions aux limites a augmenté de façon spectaculaire.
  • Avec GPT-4, on peut obtenir du JSON valide simplement en mettant des exemples dans le message système, et cela fonctionne neuf fois sur dix.
    Mais cela reste probabiliste, et neuf fois sur dix, ce n’est pas suffisant.
    Il arrive qu’il produise des réponses hallucinées du type {"key1": "value1", "key2": "value2" for i in range(n)}.
    Si on relance le prompt avec le message d’erreur de parsing, c’est généralement corrigé à la deuxième tentative.
    En revanche, l’échappement des guillemets doubles et des retours à la ligne est moins fiable. Même avec plusieurs exemples, il ne les échappe correctement qu’environ une fois sur deux, et même en relançant le prompt avec l’erreur d’échappement, le taux de réussite reste autour de 50 %.

    • La nouvelle bibliothèque TypeChat de Microsoft utilise aussi l’approche consistant à relancer le prompt lorsqu’une erreur se produit : https://github.com/microsoft/TypeChat
      Le prompt en question se trouve ici : https://github.com/microsoft/TypeChat/blob/c45460f4030938da3...
      Je trouve qu’une approche fondée sur la grammaire comme celle présentée ici, ou des méthodes comme https://github.com/ggerganov/llama.cpp/pull/1773, constituent une solution beaucoup plus élégante.
    • J’ai eu de meilleurs résultats en demandant une sortie XML plutôt que du JSON. (1) XML permet d’embarquer davantage de langage naturel et de sens, ce que les LLM apprécient, et (2) on peut rendre le parseur plus tolérant.
      Je comprends que les gens veuillent du JSON, mais pour moi c’est un peu comme vouloir faire nager un chat. On peut finir par y arriver, mais ce n’est pas son inclination naturelle.
    • En utilisant les appels de fonctions de ChatGPT, j’obtiens du JSON valide à 100 % avec GPT-4, tant que je ne fais pas d’erreur dans le prompt.
      L’erreur principale consiste à ne pas prévoir d’échappatoire. Les LLM essaient de trouver la bonne réponse ; donc si on leur donne des textes en leur demandant de renvoyer des données structurées, mais que certains textes sont vides, il devient difficile de déterminer la bonne réponse, ce qui provoque des hallucinations.
      La solution consiste à créer une échappatoire, par exemple en ajoutant parmi les arguments un booléen du type textIsMissing. Si l’on tient compte de ce mode d’échec, cela fonctionne sans accroc.
    • J’ai demandé à GPT-4 de renvoyer du code PHP d’exemple à l’intérieur d’un JSON arbitraire, et dès la première tentative, le résultat ne passait pas le linter JSON.
      Après plusieurs nouvelles tentatives et même des corrections en suivi, il n’a toujours pas passé la validation ; il n’a jamais généré une seule fois du JSON valide à 100 %, et j’ai fini par abandonner.
    • La génération contrainte par grammaire présente deux grands avantages.
      Premièrement, elle consomme moins de tokens, car il n’est pas nécessaire de mettre trop d’exemples dans le prompt.
      Deuxièmement, elle est moins affectée par le problème d’oubli.
      Comme avantage mineur, elle permet de contrôler précisément où doit commencer la sortie souhaitée. Cela dit, dans l’ensemble, je vois cela comme une bonne fonctionnalité additionnelle, pas comme quelque chose d’absolument essentiel.
  • Une grande part de la puissance des LLM réside dans la distribution de probabilité calibrée de leurs réponses, et cette technique semble probablement abandonner cette capacité. Je me demande pourquoi cela suffirait
    Prenons un exemple simple : supposons que les seules sorties possibles du LLM soient « hello world », « food », « hello » et « good day », et qu’en l’absence de prompt elles aient toutes la même probabilité. Supposons que la grammaire impose seulement la contrainte qu’il y ait un espace quelque part dans la sortie
    Si l’on échantillonne les sorties du LLM jusqu’à ce qu’elles passent la grammaire, on obtient « hello world » et « good day » avec la même probabilité. Mais avec la technique du site, « hello world » sort deux fois plus souvent que « good day »
    Le problème central est qu’un certain préfixe de réponse pouvait avoir une probabilité extrêmement faible de mener à une réponse valide, mais cette technique, en supposant qu’elle réussit, fabrique une réponse valide à partir de ce préfixe. Si les éléments sont suffisamment indépendants aux bons endroits, ça peut aller, mais dans un modèle autorégressif, les erreurs corrélées s’accumulent vite
    Pour se limiter à JSON : quand un LLM produit une réponse qui ne respecte pas le schéma, génère-t-il davantage ou moins d’erreurs factuelles, d’hallucinations, de chaînes tronquées, d’oublis de personnages principaux, etc. ? Si le taux d’erreurs factuelles est lié de façon non triviale au taux d’erreurs de schéma, cette voie est plus dangereuse qu’elle n’en a l’air. Quand on voit l’effet important que certains mots ou groupes de mots collés peuvent avoir sur la sortie d’un LLM, il paraît très probable que des détails comme le respect d’un schéma se propagent aussi à d’autres caractéristiques de la sortie

    • S’il s’agit de génération à choix multiples, comme dans ce cas, il suffit d’exclure de la génération les sorties possibles qui ne correspondent pas à l’expression régulière
      J’ai essayé d’imaginer un exemple où « un préfixe de réponse avait très peu de chances de mener à une réponse valide, mais la technique construit quand même une réponse valide et cause un problème », sans en trouver facilement. Si quelqu’un a un bon exemple, ce pourrait être une question de recherche intéressante
  • À ce sujet, LLama.cpp a implémenté le mois dernier un échantillonnage basé sur la grammaire
    https://news.ycombinator.com/item?id=36819906
    https://github.com/ggerganov/llama.cpp/pull/1773

    • Comme expliqué dans l’article ci-dessus, notre approche peut elle aussi être étendue à l’échantillonnage basé sur la grammaire. PR liée : https://github.com/normal-computing/outlines/pull/178
      Notre méthode est beaucoup plus efficace. llama.cpp parcourt à chaque étape l’ensemble du vocabulaire, soit environ 50 000 tokens, pour créer le masque
      Nous construisons un index à l’initialisation, puis à chaque étape il suffit d’effectuer une recherche dans un dictionnaire pour construire le masque. C’est un compromis vitesse contre mémoire, et l’échantillonnage est aussi rapide que l’échantillonnage standard
    • À peu près à la même période, nous avions aussi une implémentation de guidage piloté par la grammaire : https://github.com/normal-computing/outlines/pull/131
      Vu le nombre d’articles sur le sujet, il y en avait sûrement plusieurs ailleurs aussi. L’idée ici, comme dans le travail actuel, est de fournir un guidage à très faible coût ; pour les expressions régulières, c’était implémenté depuis un moment, puis nous l’avons étendu à JSON
  • Merci d’avoir créé ça ; le principe de fonctionnement est une idée tellement évidente que je suis surpris que les plateformes de premier plan ne l’aient pas encore fait
    Au-delà de JSON, je me demande comment cela pourrait être utilisé pour d’autres tâches nécessitant une entrée structurée

    • Je comprenais que la technologie des LLM était actuellement dans une course aux armements très rapide, avec des changements spectaculaires tous les quelques mois
      C’est peut-être simplement dû à des ressources de développement limitées. Si une technologie vieille de 10 ans n’avait pas ce genre de fonction de base, ce serait étonnant, mais dans une technologie IA en pleine course aux armements, l’absence actuelle de fonctionnalités de confort semble compréhensible
    • Nous avons étendu cette approche à l’échantillonnage basé sur la grammaire, comme expliqué dans l’article lié plus haut. La PR correspondante est https://github.com/normal-computing/outlines/pull/178
    • « Guiding Language Models of Code with Global Context using Monitors » sur https://arxiv.org/abs/2306.10763 montre comment faire générer du code à des modèles de langage sans déréférencements hallucinés
  • Je ne vois pas très bien en quoi cela diffère des projets suivants
    https://github.com/1rgs/jsonformer
    https://github.com/newhouseb/clownfish
    https://github.com/mkuchnik/relm
    https://github.com/ggerganov/llama.cpp/pull/1773
    https://github.com/Shopify/torch-grammar
    Globalement, il existe beaucoup de systèmes de guidage basés sur les logits de ce genre, et la raison pour laquelle ils n’obtiennent pas vraiment de traction, c’est que les modèles les plus performants sont derrière des API REST qui ne permettent pas une approche aussi fine
    Comme ces modèles sont nettement plus performants, les gens se satisfont généralement de relancer des requêtes jusqu’à obtenir le bon format. Avec GPT-4, d’après mon expérience, ces échecs sont d’ailleurs assez rares

    • Merci pour clownfish et relm, je ne les connaissais pas. À ma connaissance, les autres bibliothèques parcourent tout le vocabulaire à chaque étape de génération
      Nous parcourons le vocabulaire une seule fois à l’initialisation pour construire un index, puis la génération est aussi rapide qu’une génération standard
  • Autrement dit, est-ce que, chaque fois que le LLM génère un token, on met à jour un « masque » de biais de logits pour que le token suivant ne puisse être qu’un token JSON valide ? Très cool

    • Il faudra continuer à générer jusqu’à ce que la chaîne complète soit valide, mais je ne sais pas ce qui se passe si ça part dans une boucle
      Je ne suis pas sûr de comprendre comment cette méthode peut vraiment garantir du 100 %
    • Oui. Et on peut mettre à jour le masque par recherche dans un dictionnaire, sans parcourir tout le vocabulaire. Parcourir tout le vocabulaire est lent
    • Comme les tokens JSON ne correspondent pas exactement aux logits, il faut aussi une forme de beam search ou d’échantillonnage par rejet
      Édition : l’article explique ce point avec plus de prudence
    • En fait, c’est une technique très ancienne, et beaucoup de bibliothèques font ça. Je ne vois pas bien ce que cette bibliothèque a de si extraordinaire
  • Ce Brandon Willard est-il le breakdancer Brandon Willard de Detroit ?
    Édition : oui ! https://brandonwillard.github.io/

    • Oui. C’était il y a assez longtemps, mais c’était vraiment amusant
  • remilouf, puisque vous dites que votre parcours est en « programmation probabiliste, relationnelle et symbolique », j’imagine que vous comprenez que générer du texte à partir de grammaires régulières, de grammaires hors contexte, ou en pratique de grammaires de n’importe quel niveau, n’est pas le problème
    Par exemple, dans Prolog, un langage relationnel, c’est très facile à faire avec la notation Definite Clause Grammars dès lors qu’on fournit seulement la grammaire
    À mon avis, cette approche exige que l’utilisateur fournisse la grammaire. Dans ce cas, je me demande quel est l’intérêt d’utiliser un LLM pour générer du texte
    Pourquoi ne pas simplement exécuter la grammaire comme générateur pour produire le texte voulu ? Cela éviterait d’emblée le gros effort et le coût de l’entraînement d’un LLM. Et si l’objectif est seulement de générer du texte structuré, je me demande aussi pourquoi un LLM, qui est un modèle de langage naturel, est nécessaire

    • On n’obtiendrait alors pas une sortie valide mais complètement aléatoire ? Ici, il faut une sortie valide pertinente par rapport à la requête
      Même si l’on veut seulement du texte structuré, la raison pour laquelle un LLM est nécessaire est de parser du texte non structuré écrit par des humains et de renvoyer des données structurées exploitables par une machine
    • Le but n’est pas de générer n’importe quelle chaîne aléatoire conforme à la grammaire
      Si la requête est « quelles sont les 10 premières décimales de pi ? » et que la réponse est limitée par l’expression régulière "[0-9]+\.[0-9]+", l’objectif est d’obtenir la vraie réponse, 3.1415926535, et pas simplement une chaîne aléatoire conforme au motif, comme « 1.2346789 »
    • IanCal a tout dit. Cela dit, une approche alternative utilisant des LLM avec miniKanren peut être consultée ici : https://arxiv.org/abs/1809.02840
  • Intéressant ; récemment, nous aussi avons créé un outil similaire qui contraint les sorties de llama à correspondre à une interface TypeScript[1]
    Je suis fermement convaincu que la garantie du format de sortie deviendra importante au cours des prochaines décennies, à mesure que les LLM seront utilisés pour de vrais cas d’usage non triviaux
    [1] https://github.com/ggerganov/llama.cpp/discussions/2494