1 Punkte von GN⁺ 2023-08-15 | 1 Kommentare | Auf WhatsApp teilen
  • Outlines ist ein Tool für strukturierte Ausgaben von LLMs und stellt sicher, dass Daten exakt der gewünschten Struktur entsprechen, indem der gewünschte Ausgabetyp während der Generierung festgelegt wird
  • Statt fehlerhafte Ausgaben nach der Generierung mit Parsing, regulären Ausdrücken oder fragilen Codekonstrukten zu korrigieren, erzwingt es gültige Strukturen direkt im Generierungsschritt
  • Die Nutzung erfolgt in der Form model(prompt, output_type) und erlaubt die Festlegung der Ausgabe passend zum Python-Typsystem, etwa mit Literal["Yes", "No"], int oder einem Pydantic-Modell
  • Zu den unterstützten Ausgabetypen gehören vordefinierte Auswahloptionen, Function Calls auf Basis von Funktionssignaturen, JSON-/Pydantic-Schemas, reguläre Ausdrucksmuster und strukturgebende Einschränkungen auf Basis von Grammatiken
  • Die Modellintegration ist unterteilt in serverunterstützte vLLM- und Ollama-Modelle, lokal unterstützte Modelle über transformers und llama.cpp sowie API-unterstützte OpenAI-, Gemini- und Dottxt-Modelle
  • Beispiel-Workflows sind das Umwandeln von Kunden-E-Mails in Service-Tickets, das Überführen von Produktbeschreibungen in Kategoriedaten, das Extrahieren strukturierter Informationen aus unvollständigen Ereignisbeschreibungen oder die Rückgabe von "I don't know", die Klassifizierung von Dokumenten in vordefinierte Kategorien sowie das Umwandeln natürlicher Besprechungsanfragen in Funktionsparameter
  • Prompt-Templates unterstützen das auf Jinja basierende outlines.Template.from_string sowie das Laden aus Dateien, um komplexe Prompts vom Code zu trennen und wiederzuverwenden
  • Die Installation erfolgt über pip install outlines, und die .txt API ist derzeit als Early Access gekennzeichnet

1 Kommentare

 
GN⁺ 2023-08-15
Meinungen auf Hacker News
  • Diese Bibliothek scheint vom Mechanismus her die einfache Idee zu verwenden, Teile des Vokabularraums zu maskieren und effizient Schritt für Schritt über die Zeitschritte vorzugehen; das ist großartig.
    Aus der Perspektive von jemandem, der Bibliotheken ausprobiert hat, die einem Basis-LLM eine Ausgabestruktur aufzwingen, frage ich mich allerdings, ob Basismodelle wie Llama2 wirklich gut funktionieren.
    Meiner Erfahrung nach ist die Antwort eher „überhaupt nicht“, und damit es in der Praxis funktioniert, war ziemlich viel Instruction Tuning für den jeweiligen Anwendungsfall nötig.
    Außerdem wirkt es kontraintuitiv, dass das nachträgliche Maskieren des Zustandsraums während der Generierung bei einem instruction-getunten Modell letztlich die Generierungsverteilung verändert und damit dem Instruction Tuning sogar schaden könnte.

    • Ich sehe nicht recht, warum man unbedingt das ursprüngliche llama-2 verwenden sollte. Auf dem HF Hub gibt es sehr viele starke Varianten von llama-2, die per Instruction Fine-Tuning angepasst wurden, und diese Modelle dürften die Aufgabe viel besser erledigen. Ein Beispiel ist Beluga-2 von Stability-AI: https://huggingface.co/spaces/HuggingFaceH4/open_llm_leaderb...
      Zum zweiten Punkt: Wenn das Ziel beispielsweise ist, das Modell dazu zu bringen, nur JSON zu erzeugen, dann ist das zu 100 % möglich, indem man einschränkt, welche Ausgabetokens verwendet werden dürfen und welche nicht.
    • Von Llama 2 13B bin ich ziemlich beeindruckt, und je länger ich es benutze, desto mehr denke ich, dass es vielleicht mehr als nur ein lokales LLM-Spielzeug ist und tatsächlich nützlich sein könnte.
      Auf einem M2 Mac kann ich die GPU nutzen, daher verwende ich die MLC-Version als Plugin über https://github.com/simonw/llm-mlc.
    • Zumindest bei der Codegenerierung konnte man Basismodelle deutlich verbessern, wenn man die Generierung auf Token-Ebene steuert.
      Im Paper „Guiding Language Models of Code with Global Context using Monitors“ (https://arxiv.org/abs/2306.10763) wurde Monitor Guided Decoding vorgeschlagen, bei dem ein LLM mit statischer Analyse verbunden wird, um es zur Erzeugung typkonsistenten Codes anzuleiten.
      Ohne jegliches Fine-Tuning verbesserte die Steuerung der Token-Level-Generierung durch statische Analyse an bestimmten Stellen die Qualität des generierten Codes deutlich, sowohl bei der Kompilierbarkeit als auch bei der Übereinstimmung mit der richtigen Antwort. Selbst sehr kleine Modelle (1,1B) erzeugten mehr kompilierbaren Code als viel größere Modelle (175B) und verbesserten zugleich die Übereinstimmung mit der richtigen Antwort.
    • Wenn „nachträgliches Maskieren während der Generierung bei einem instruction-getunten Modell die Generierungsverteilung verändert“, dann frage ich mich, ob das nicht genau das ist, was man bei testgetriebener Entwicklung schon immer gemacht hat.
      Der Hauptunterschied ist nur, dass die Generierungsfunktion kein LLM war, sondern ein Mensch. Ich sehe keinen Grund, warum man den Menschen in der Mitte nicht herausnehmen sollte.
    • Instruction Tuning selbst ist eher „trivial“; wirklich schwierig ist der Umgang mit Randbedingungen.
      In klassischem Code waren Randbedingungen buchstäblich eher kleine Sonderfälle, aber bei LLMs weiß man nicht, was sie in eine völlig falsche Richtung abdriften lässt, und der Parsercode muss mit diesem Chaos zurechtkommen.
      Anders gesagt: Der Anteil der Fälle, die man als Randbedingungen behandeln muss, scheint dramatisch gestiegen zu sein.
  • GPT-4 kann man allein mit Beispielen in der System Message dazu bringen, gültiges JSON zurückzugeben, und in neun von zehn Fällen funktioniert das.
    Aber es bleibt probabilistisch, und neun von zehn Fällen reichen nicht aus.
    Gelegentlich erzeugt es auch halluzinierte Antworten wie {"key1": "value1", "key2": "value2" for i in range(n)}.
    Wenn man es mit der Parsing-Fehlermeldung erneut promptet, ist es beim zweiten Versuch normalerweise gelöst.
    Das Escapen von doppelten Anführungszeichen und Zeilenumbrüchen ist allerdings weniger zuverlässig. Selbst mit mehreren Beispielen wird nur etwa die Hälfte korrekt escaped, und auch wenn man Escaping-Fehler erneut promptet, bleibt die Erfolgsrate bei ungefähr 50 %.

    • Die Strategie, bei einem Fehler erneut zu prompten, verwendet auch Microsofts neue Bibliothek TypeChat: https://github.com/microsoft/TypeChat
      Der betreffende Prompt ist hier: https://github.com/microsoft/TypeChat/blob/c45460f4030938da3...
      Ich halte den hier gezeigten grammatikbasierten Ansatz oder Verfahren wie https://github.com/ggerganov/llama.cpp/pull/1773 für eine deutlich elegantere Lösung.
    • XML-Ausgaben funktionieren bei mir besser als JSON. (1) In XML kann man mehr natürliche Sprache und Bedeutung unterbringen, und LLMs mögen so etwas, und (2) man kann den Parser toleranter machen.
      Ich verstehe, dass Leute JSON wollen, aber für mich ist das ein bisschen so, als wollte man eine Katze zum Schwimmen bringen. Am Ende kann es gelingen, aber es entspricht nicht ihrer natürlichen Veranlagung.
    • Mit Function Calling von ChatGPT bekomme ich bei GPT-4 zu 100 % gültiges JSON, solange ich im Prompt keinen Fehler mache.
      Der wichtigste Fehler ist, keinen Ausweg anzubieten. Da ein LLM versucht, die richtige Antwort zu finden, wird es schwierig, die richtige Antwort zu bestimmen, wenn man Texte hineingibt und strukturierte Daten zurückverlangt, aber einer der Texte leer ist; dann entstehen Halluzinationen.
      Die Lösung besteht darin, einen Ausweg zu schaffen, etwa indem man eines der Argumente als Boolean wie textIsMissing anlegt. Wenn man solche Fehlermuster berücksichtigt, funktioniert es tadellos.
    • Ich habe GPT-4 gebeten, PHP-Beispielcode in einem beliebigen JSON zurückzugeben, und schon der erste Versuch kam nicht durch einen JSON-Linter.
      Nach mehreren erneuten Versuchen und anschließenden Korrekturen bestand es die Validierung immer noch nicht, und weil es kein einziges Mal zu 100 % gültiges JSON erzeugte, habe ich schließlich aufgegeben.
    • Grammatikbeschränkte Generierung hat zwei große Vorteile.
      Erstens muss man nicht so viele Beispiele in den Prompt packen und verbraucht dadurch weniger Tokens.
      Zweitens ist sie weniger anfällig für das Vergessensproblem.
      Ein kleinerer Vorteil ist, dass man genau steuern kann, wo die gewünschte Ausgabe beginnen soll. Insgesamt sehe ich es allerdings eher als nützliche Zusatzfunktion und nicht als etwas absolut Wesentliches.
  • Ein großer Teil der Stärke von LLMs liegt in der kalibrierten Wahrscheinlichkeitsverteilung ihrer Antworten; diese Technik scheint diese Fähigkeit vermutlich aufzugeben. Ich frage mich, warum das ausreichen soll.
    Nehmen wir als einfaches Beispiel an, die möglichen Ausgaben eines LLM seien nur „hello world“, „food“, „hello“ und „good day“, und ohne Prompt hätten alle dieselbe Wahrscheinlichkeit. Angenommen, die Grammatik beschränkt sich darauf, dass irgendwo in der Ausgabe ein Leerzeichen vorkommen muss.
    Wenn man LLM-Ausgaben sampelt, bis sie die Grammatik erfüllen, erhält man „hello world“ und „good day“ mit gleicher Wahrscheinlichkeit. Wendet man jedoch die auf der Website beschriebene Technik an, erscheint „hello world“ doppelt so häufig wie „good day“.
    Das Kernproblem ist: Auch wenn ein bestimmtes Antwortpräfix nur mit extrem geringer Wahrscheinlichkeit zu einer gültigen Antwort geführt hätte, konstruiert diese Technik — unter der Annahme, dass sie erfolgreich ist — aus diesem Präfix eine gültige Antwort. Wenn die Dinge an den richtigen Stellen hinreichend unabhängig sind, mag das in Ordnung sein; bei autoregressiven Modellen kumulieren korrelierte Fehler jedoch schnell.
    Auf JSON beschränkt gefragt: Erzeugt ein LLM, wenn es Antworten produziert, die das Schema nicht einhalten, eher mehr oder eher weniger sachliche Fehler, Halluzinationen, abgeschnittene Strings, fehlende Hauptfiguren usw.? Wenn die Rate sachlicher Fehler auf nichttriviale Weise mit der Schemafehlerrate zusammenhängt, ist dieser Weg riskanter, als er aussieht. Wenn man sieht, wie stark bestimmte Wörter oder zusammengeklebte Wortgruppen die Ausgabe eines LLM beeinflussen, wirkt es sehr wahrscheinlich, dass sich Details wie Schema-Konformität auch auf andere Eigenschaften der Ausgabe auswirken.

    • Wenn es sich wie in diesem Fall um Multiple-Choice-Generierung handelt, kann man mögliche Ausgaben, die nicht zur Regex passen, einfach von der Generierung ausschließen.
      Ich habe versucht, mir ein Beispiel vorzustellen, bei dem „ein Antwortpräfix zwar nur mit sehr geringer Wahrscheinlichkeit zu einer gültigen Antwort geführt hätte, die Technik aber irgendwie eine gültige Antwort konstruiert und dadurch Probleme verursacht“, mir fällt aber keines wirklich ein. Wenn es ein gutes Beispiel gibt, wäre das eine interessante Forschungsfrage.
  • Passend dazu hat LLama.cpp letzten Monat grammatikbasiertes Sampling implementiert.
    https://news.ycombinator.com/item?id=36819906
    https://github.com/ggerganov/llama.cpp/pull/1773

    • Wie in dem oben genannten Paper beschrieben, lässt sich auch unser Ansatz auf grammatikbasiertes Sampling erweitern. Zugehöriger PR: https://github.com/normal-computing/outlines/pull/178
      Unser Verfahren ist deutlich effizienter. llama.cpp iteriert bei jedem Schritt über das gesamte Vokabular von rund 50.000 Tokens, um eine Maske zu erstellen.
      Wir erstellen bei der Initialisierung einen Index; um bei jedem Schritt die Maske zu konstruieren, genügt anschließend ein Dictionary-Lookup. Das tauscht Geschwindigkeit gegen Speicher, und das Sampling ist so schnell wie Standard-Sampling.
    • In einem ähnlichen Zeitraum hatten auch wir eine Implementierung für grammatikgesteuerte Führung: https://github.com/normal-computing/outlines/pull/131
      Angesichts der vielen einschlägigen Paper dürfte es auch anderswo mehrere Ansätze gegeben haben. Der Punkt hier und bei der aktuellen Arbeit ist, Guidance mit sehr geringen Kosten bereitzustellen; für den Regex-Fall war das schon eine Weile implementiert und wurde später auf JSON erweitert.
  • Danke, dass ihr so etwas gebaut habt; die Funktionsweise ist eine so naheliegende Idee, dass es überrascht, dass die First-Party-Plattformen das noch nicht anbieten.
    Neben JSON frage ich mich, wie sich das für andere Aufgaben nutzen lässt, die strukturierte Eingaben benötigen.

    • Nach meinem Verständnis befindet sich die LLM-Technologie derzeit in einem sehr schnellen Wettrüsten und verändert sich alle paar Monate dramatisch.
      Es könnte auch einfach an begrenzten Entwicklerressourcen liegen. Bei einer zehn Jahre alten Technologie wäre es überraschend, wenn eine solche Grundfunktion fehlt; bei einer KI-Technologie mitten im Wettrüsten wirkt es nachvollziehbar, dass noch Komfortfunktionen fehlen.
    • Wir haben diesen Ansatz auf grammatikbasiertes Sampling erweitert und beschreiben das in dem oben verlinkten Paper. Der zugehörige PR ist https://github.com/normal-computing/outlines/pull/178.
    • „Guiding Language Models of Code with Global Context using Monitors“ unter https://arxiv.org/abs/2306.10763 zeigt, wie man Sprachmodelle dazu bringt, Code ohne halluzinierte Dereferenzierungen zu generieren.
  • Mir ist nicht ganz klar, wie sich das von den folgenden Projekten unterscheidet:
    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
    Insgesamt gibt es sehr viele solcher logitbasierten Guidance-Systeme, und der Grund, warum sie nicht viel Traktion bekommen, ist wohl, dass die leistungsstärksten Modelle hinter REST-APIs liegen, die solche feingranularen Ansätze nicht erlauben.
    Diese Modelle sind so viel leistungsfähiger, dass die Leute im Allgemeinen damit zufrieden sind, erneut anzufragen, bis das richtige Format herauskommt. Bei GPT-4 sind solche Fehlschläge meiner Erfahrung nach ohnehin ziemlich selten.

    • Danke für den Hinweis auf clownfish und relm. Soweit ich weiß, iterieren die anderen Libraries bei jedem Generierungsschritt über das gesamte Vokabular.
      Wir iterieren bei der Initialisierung einmal über das Vokabular, um einen Index zu erstellen; danach ist die Generierung so schnell wie Standard-Generierung.
  • Anders ausgedrückt: Wird bei jedem Token, das das LLM erzeugt, die Logit-Bias-„Maske“ so aktualisiert, dass das nächste Token nur ein gültiges JSON-Token sein kann? Sehr cool

    • Man müsste ja so lange weitergenerieren, bis der gesamte String gültig ist; ich weiß nicht, was passiert, wenn man in eine Schleife gerät
      Ich bin nicht sicher, wie diese Methode wirklich 100 % garantieren kann
    • Genau. Und man kann die Maske per Dictionary-Lookup aktualisieren, ohne das gesamte Vokabular zu durchlaufen. Ein vollständiger Durchlauf ist langsam
    • Da JSON-Tokens nicht exakt den Logits entsprechen, braucht man auch irgendeine Form von Beam Search oder Rejection Sampling
      Edit: Im Paper wird dieser Teil vorsichtiger erklärt
    • Eigentlich ist das eine sehr alte Technik, und viele Libraries machen das. Ich bin mir nicht sicher, was an dieser Library so besonders sein soll
  • Ist dieser Brandon Willard der Breakdancer Brandon Willard aus Detroit?
    Edit: Ja! https://brandonwillard.github.io/

    • Ja. Das ist zwar schon ziemlich lange her, aber es hat wirklich Spaß gemacht
  • remilouf, da du deinen Hintergrund als „probabilistisches, relationales und symbolisches Programmieren“ beschrieben hast, nehme ich an, dass du verstehst, dass das Erzeugen von Text aus regulären Grammatiken, kontextfreien Grammatiken oder praktisch jeder Grammatikstufe kein Problem ist
    In einer relationalen Sprache wie Prolog geht das zum Beispiel sehr einfach, wenn man die Grammatik in der Notation der Definite Clause Grammars vorgibt
    Aus meiner Sicht erfordert dieser Ansatz, dass der Nutzer die Grammatik bereitstellt. Dann frage ich mich, was der Vorteil ist, für die Texterzeugung ein LLM zu verwenden
    Warum nicht einfach die Grammatik als Generator ausführen und den gewünschten Text erzeugen? Dann könnte man sich von vornherein den großen Aufwand und die Kosten sparen, ein LLM zu trainieren. Wenn man nur strukturierten Text erzeugen will, frage ich mich auch, warum man ein LLM als Natural-Language-Modell überhaupt braucht

    • Dann bekommt man doch eine völlig zufällige, aber gültige Ausgabe, oder? Hier braucht man eine gültige Ausgabe, die zur Anfrage passt
      Selbst wenn man nur strukturierten Text will, braucht man ein LLM, um von Menschen geschriebenen unstrukturierten Text zu parsen und maschinenverwendbare strukturierte Daten zurückzugeben
    • Das Ziel ist nicht, irgendeinen zufälligen String zu erzeugen, der zur Grammatik passt
      Wenn die Anfrage lautet: „Was sind die ersten 10 Ziffern von Pi?“ und die Antwort durch den regulären Ausdruck "[0-9]+\.[0-9]+" eingeschränkt ist, dann will man nicht irgendeinen zufälligen String wie „1.2346789“, der nur dem Muster entspricht, sondern die tatsächliche Antwort 3.1415926535
    • IanCal hat schon alles gesagt. Einen alternativen Ansatz, LLMs zusammen mit miniKanren zu verwenden, findet man allerdings unter https://arxiv.org/abs/1809.02840
  • Interessant; wir haben vor Kurzem auch ein ähnliches Tool gebaut, das die Ausgabe von llama so einschränkt, dass sie zu einem TypeScript-Interface passt[1]
    Ich bin fest davon überzeugt, dass Garantien für das Ausgabeformat in den kommenden Jahrzehnten wichtig werden, wenn LLMs für echte, nicht spielzeughafte Anwendungsfälle eingesetzt werden
    [1] https://github.com/ggerganov/llama.cpp/discussions/2494