Show HN: Outlines - LLMが有効なJSONなどの構造化出力を生成することを保証するツール
(github.com/normal-computing)- OutlinesはLLM向けの構造化出力ツールで、生成時に望む出力型を指定して、データがその構造に正確に一致することを保証する
- パースや正規表現、壊れやすいコードで生成後に誤った出力を修正するのではなく、生成段階で有効な構造を直接強制する
- 使用方法は
model(prompt, output_type)の形式で、Literal["Yes", "No"]、int、Pydantic modelのようなPythonの型体系に合わせた出力指定が可能 - 対応する出力タイプは、事前定義された選択肢、関数シグネチャベースのFunction Calls、JSON/Pydanticスキーマ、正規表現パターン、文法(grammars)ベースの構造強制
- モデル統合は、サーバー対応のvLLM・Ollama、ローカルモデル対応のtransformers・llama.cpp、API対応のOpenAI・Gemini・Dottxtに分かれる
- 例のワークフローとしては、顧客メールをサポートチケットに変換、商品説明をカテゴリデータに変換、不完全なイベント説明から構造化情報または
"I don't know"を返す、文書を事前定義カテゴリに分類、自然言語の会議依頼を関数パラメータに変換する、といったものがある - プロンプトテンプレートはJinjaベースの
outlines.Template.from_stringとファイル読み込みをサポートし、複雑なプロンプトをコードから分離して再利用する用途に向く - インストールは
pip install outlinesで提供され、.txt APIは現在early accessの状態とされている
1件のコメント
Hacker News のコメント
このライブラリは、仕組みとしては語彙空間の一部をマスクし、時間ステップごとに効率よく進めるというシンプルなアイデアを使っているようで、素晴らしいと思う。
ただ、ベースの LLM に出力構造をかぶせるライブラリを使ったことがある立場からすると、Llama2 のようなベースモデルで本当にうまく動くのかは気になる。
私の経験では「まったくそうではない」に近く、実際に動くようにするには、特定用途向けの指示チューニングがかなり必要だった。
さらに、指示チューニング済みモデルに対して生成中に事後的に状態空間をマスクするのは、結局のところ生成分布を変えることになるので、指示チューニングに悪影響を与える可能性もあるという点が、直感に反しているように見える。
2つ目の論点については、目的がたとえばモデルに JSON だけを生成させることなら、どの出力トークンを使えて、どれを使えないかを制限する方法で 100% 可能だ。
M2 Mac で GPU を使えるので、MLC 版を https://github.com/simonw/llm-mlc プラグインで使っている。
論文 “Guiding Language Models of Code with Global Context using Monitors”(https://arxiv.org/abs/2306.10763) では Monitor Guided Decoding を提案しており、LLM を静的解析とつなげて、型の整合性があるコードを生成するよう誘導している。
いかなるファインチューニングもなしに、特定の地点で静的解析によってトークンレベルの生成を誘導すると、コンパイル可能性と正解との一致度の両方で、生成コードの品質が大きく向上した。非常に小さなモデル(1.1B)でも、はるかに大きなモデル(175B)よりコンパイル可能なコードを多く作りつつ、正解一致度も改善した。
主な違いは、生成関数が LLM ではなく人間だったという点だけだ。途中の人間を抜いてはいけない理由はなさそうだ。
従来のコードでは境界条件は文字どおり小さな特殊ケースに近かったが、LLM では何が妙な方向に飛ばすのか分からず、パース用コードはその混沌に対処しなければならない。
言い換えると、境界条件として扱うべきケースの割合が劇的に上がったように感じる。
GPT-4 は、システムメッセージに例を入れるだけで有効な JSONを返すようにでき、10回中9回は動作する。
しかし依然として確率的で、10回中9回では十分ではない。
ときどき
{"key1": "value1", "key2": "value2" for i in range(n)}のような幻覚応答を作ることもある。パースエラーのメッセージと一緒に再プロンプトすると、たいてい2回目の試行では解決する。
ただし、ダブルクォートと改行文字をエスケープするのはあまり安定しない。複数の例を与えても半分程度しか正しくエスケープせず、エスケープエラーを再プロンプトしても成功率は約50%にとどまる。
そのプロンプトはここにある: https://github.com/microsoft/TypeChat/blob/c45460f4030938da3...
ここで見られる文法ベースのアプローチや https://github.com/ggerganov/llama.cpp/pull/1773 のような方法のほうが、はるかに優雅な解決策だと思う。
人々が JSON を欲しがるのは理解できるが、私には猫を泳がせようとしているように見える。最終的には成功するかもしれないが、自然な性向ではない。
主なミスは逃げ道を用意しないことだ。LLM は正しい答えを見つけようとするので、テキスト群を入れて構造化データを返すように言ったとき、あるテキストが空だと正しい答えを決めにくくなり、幻覚が生じる。
解決策は、引数の1つに
textIsMissingのようなブール値を置く形で逃げ道を作ることだ。こうした失敗パターンを考慮しておけば、問題なく動作する。何度も再試行し、追加の修正までさせたが検証を通過せず、100% 有効な JSON を一度も生成できなかったので、結局あきらめた。
第一に、プロンプトにあまり多くの例を入れる必要がないので、トークン消費が少ない。
第二に、忘却問題の影響を受けにくい。
小さな利点としては、望む出力をどこから開始すべきか正確に制御できる。ただし全体としては、よい追加機能ではあるが、非常に本質的なものだとは見ていない。
LLMの力の大きな部分は、応答の較正された確率分布にありますが、この手法はおそらくその能力を捨てているように見えます。なぜこれで十分なのか疑問です。
簡単な例として、LLMの取り得る出力が「hello world」「food」「hello」「good day」だけで、プロンプトがないときはいずれも同じ確率だとしましょう。文法は、出力のどこかに空白がなければならない、という制約だけを課すと仮定します。
文法を通過するまでLLMの出力をサンプリングすれば、「hello world」と「good day」を同じ確率で受け取ることになります。しかし、このウェブサイトの手法を適用すると、「hello world」は「good day」より2倍多く出てきます。
核心的な問題は、ある回答プレフィックスが有効な応答につながる可能性は極めて低かったかもしれないのに、この手法は成功すると仮定したうえで、そのプレフィックスから有効な応答を作り出してしまう点です。適切な箇所で十分に独立していれば問題ないかもしれませんが、自己回帰モデルでは相関した誤りが急速に蓄積します。
JSONに限って問うなら、スキーマを守れない応答を作るとき、LLMは事実誤り、ハルシネーション、途中で切れた文字列、主要人物の欠落などをより多く作るのでしょうか、それとも少なく作るのでしょうか。事実誤り率がスキーマ誤り率と非自明に関連しているなら、この道は見た目より危険です。特定の単語やくっついてしまった語句がLLMの出力に大きな影響を与えることを考えると、スキーマ準拠のような細部も出力の他の特性へ波及する可能性は高そうです。
「回答プレフィックスが有効な応答につながる可能性は非常に低かったが、手法が何とか有効な応答を組み立てて問題を起こす」例を考えようとしましたが、うまく思い浮かびません。良い例があれば、興味深い研究課題になり得ます。
関連して、LLama.cppが先月、文法ベースのサンプリングを実装しました。
https://news.ycombinator.com/item?id=36819906
https://github.com/ggerganov/llama.cpp/pull/1773
私たちの方式のほうがはるかに効率的です。llama.cppは各ステップでマスクを作るために、約5万トークンの語彙全体を走査します。
私たちは初期化時にインデックスを作り、各ステップでマスクを構成するときは辞書ルックアップを行うだけです。速度をメモリと引き換えにする方式で、サンプリングは標準的なサンプリングと同じ速さです。
関連論文が多かったことを考えると、他にも複数あったはずです。ここおよび現在の作業の要点は、非常に低コストな誘導を提供することで、正規表現のケースではしばらく前から実装されており、その後JSONへ拡張しました。
こういうものを作ってくれてありがたいですし、動作原理はあまりに当然のアイデアなので、ファーストパーティのプラットフォームがまだやっていないことに驚きます。
JSON以外にも、構造化入力が必要な他のタスクにどう使えるのか気になります。
開発者リソースが限られている結果かもしれません。10年ものの技術にこうした基本機能が欠けていたら驚きますが、軍拡競争中のAI技術でまだ便利機能が欠けているのは納得できるように思えます。
これが次のプロジェクト群とどう違うのか、よく分かりません。
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
全般に、こうしたロジットベースの誘導システムは非常に多くありますが、大きな支持を得ていない理由は、最高性能のモデルがこのような細かなアプローチを許さないREST APIの背後にあるからです。
それらのモデルははるかに性能が良いので、人々はたいてい正しい形式が出るまで再リクエストする方式で満足しています。GPT-4では、私の経験上そうした失敗もかなりまれです。
私たちは初期化時に語彙を一度走査してインデックスを作り、その後の生成は標準的な生成と同じ速さです。
別の言い方をすると、LLM がトークンを1つ生成するたびに、次のトークンが有効な JSON トークンだけになるようロジットバイアスの「マスク」を更新している、ということ? すごくクール
この方式がどうやって本当に 100% を保証できるのか確信が持てない
追記: 論文ではこの部分をより慎重に説明している
この Brandon Willard は、Detroit のブレイクダンサー Brandon Willard なのか?
追記: そうだった! https://brandonwillard.github.io/
remilouf は、自分のバックグラウンドが「確率的、関係的、記号的プログラミング」だと言っているので、正規文法や文脈自由文法、実質的にどんなレベルの文法でもテキストを生成すること自体は問題ではないと理解しているのだと思う
例えば関係型言語である Prolog では、Definite Clause Grammars 記法で文法さえ与えられれば、非常に簡単にできる
私には、このアプローチではユーザーが文法を提供する必要があるように見える。だとすると、テキスト生成にLLMを使う利点は何なのか気になる
単に文法を生成器として実行して、欲しいテキストを作ればよいのではないだろうか? そうすれば、そもそも LLM を訓練する大きな手間とコストを減らせる。構造化テキストだけを生成したいなら、自然言語モデルである LLM がなぜ必要なのかも疑問だ
構造化テキストだけが欲しいとしても LLM が必要な理由は、人間が書いた非構造化テキストをパースして、機械が使える構造化データを返すためだ
リクエストが「円周率の最初の10桁は?」で、応答を正規表現
"[0-9]+\.[0-9]+"に制限したなら、単にパターンに合う「1.2346789」のようなランダム文字列ではなく、実際の正解である 3.1415926535 を受け取ることが目標だ興味深いし、最近われわれも llama の出力が TypeScript インターフェースに合うよう制限する似たツールを作った[1]
出力形式の保証は、LLM が実際におもちゃではない用途で使われる今後数十年にわたって重要になると強く信じている
[1] https://github.com/ggerganov/llama.cpp/discussions/2494