見出し画像

[Guidance#6]構文エラーが発生しないJSONデータの生成を試す

LLMを使ったアプリケーションを開発する際に、プログラム上で扱いやすいテキストを生成したいという状況はたびたび発生してきます。

このような時に、よくJSONあたりの形式で出力してもらうようにプロンプトを調整していくのですが、やはり想定した出力形式にならずエラーが発生するというケースがありました。

Guidanceはこのような課題を解決する方法を提案しています。


構文エラーが発生しない生成方法

簡潔にいうと、先にフォーマットを作っておいて、値の部分だけテキストを生成するというアプローチです。つまり構文のところはLLMによる生成に依存しないため、ほぼエラーが出ないようになっています。

公式notebookがありましたので、こちらを参考に日本語版で試してみました。ただし5/24現在、上記の公式notebookはエラーが発生する箇所がありますので、本記事のプログラムを参考にすることを推奨します。

以下のColabから試せます。(llama-7bを利用しており、システムRAMが28.1GBほど求められるため、GPU A100で利用する必要があります。他に良さそうなLLMがあればぜひ教えてください!)

使用時のリソース状況(右サイドバーを参考にしてください)


プログラムを定義する

キャラクターのプロフィール情報を、JSON文字列で生成するためのプロンプトを定義しました。guidance()の中のプロンプトがそれに当たります。

{{gen …}}のところで、値の部分のみをLLMで生成しています。

!pip install guidance transformers

!pip show guidance
> Version: 0.0.55

import guidance

# 今回はLLaMA-7Bを利用します。他のGPTモデルでも可能
guidance.llm = guidance.llms.Transformers("huggyllama/llama-7b", device=0)

# オプションを事前定義
valid_weapons = ["sword", "axe", "mace", "spear", "bow", "crossbow"]

# プロンプトを定義
program = guidance("""The following is a character profile for an RPG game in JSON format.
```json
{
    "description": "{{description}}",
    "name": "{{gen 'name'}}",
    "age": {{gen 'age' pattern='[0-9]+' stop=','}},
    "armor": "{{#select 'armor'}}leather{{or}}chainmail{{or}}plate{{/select}}",
    "weapon": "{{select 'weapon' options=valid_weapons}}",
    "class": "{{gen 'class'}}",
    "mantra": "{{gen 'mantra'}}",
    "strength": {{gen 'strength' pattern='[0-9]+' stop=','}},
    "items": [{{#geneach 'items' num_iterations=3}}
        "{{gen 'this'}}",{{/geneach}}
    ]
}```""")

# プロンプトの実行
out = program(description="A quick and nimble fighter.", valid_weapons=valid_weapons)
  • 値を生成する

    • {{gen '変数名'}} でテキストを生成され、変数名に生成テキストがキャッシュされます

  • 指定した選択肢の中から生成する

    • {{#select '変数名'}}A{{or}}B{{or}}C{{/select}} でいずれかの項目に当てはまるテキストが生成されます

    • {{select '変数名' options=list_var}} で事前に項目をリスト型で定義したものを指定することも可能です

  • 数値データを生成する

    • {{gen '変数名' pattern='[0-9]+' stop=','}}で定義できます

      • pattern=には正規表現が入り、加えて桁数なども強制することができます。詳細はこちらにまとめてます。

      • また数値データの場合、終了文字列としてstop=','を明記する必要があります

  • リスト型のデータを生成する

    • [{{#geneach 'items' num_iterations=3}} "{{gen 'this'}}", {{/geneach}} ]でリスト型の文字列を生成することができます

      • num_iterationsでリストの個数を指定できます

      • {{#geneach…}} {{/geneach}}で囲まれた部分が繰り返し生成する部分になります

      • リストの中身を "{{gen 'this'}}", と指定することで、ダブルクオート(")で囲まれてカンマ(,)で区切られた文字列を静的に定義することができます


出力結果からJSONデータを取得する

取得方法は二つあります。

  1. 生成された文字列からjson部分のみをパースして取得する

  2. 生成された文字列を辞書形式で取得し、jsonへ変換する

個人的には、2番目の方法がおすすめです。後からJSONの形式を自在にカスタマイズできたりするので、かなり有用だと感じています。

それぞれ見ていきましょう。

1. 生成された文字列からjson部分のみをパースして取得する

こちらは従来よく利用されてきた方法です。```jsonの文字列を起点に分割を行なって、jsonデータ部分だけを抜き出すアプローチです。

# 生成されたテキストと、静的なテキストの混合からなる有効なjson文字列を生成
print(str(out).split("```json")[1][:-3])

{
"description": "A quick and nimble fighter.",
"name": "Ranger",
"age": 20,
"armor": "leather",
"weapon": "bow",
"class": "ranger",
"mantra": "I am the hunter",
"strength": 10,
"items": [
"bow",
"dagger",
"dagger",
]
}

出力結果

この ```json   ``` 部分は静的に定義されているものであるため、必ずjson箇所を取得することができます。従来の方法だと、この枠部分も生成する必要があったため、プロンプトによって制御する必要がありました。


2. 生成された文字列を辞書形式で取得し、jsonへ変換する

Guidanceプログラムによって生成された文字列は、すべてキャッシュされます。キャッシュされたデータは、変数名をKey、生成文字列をValueとした辞書形式で取得ができます。

# out.variables()の戻り値は、pythonの辞書型データです
out.variables()

{'llm': <guidance.llms._transformers.Transformers at 0x7fe30aa35f90>, 'description': 'A quick and nimble fighter.', 'valid_weapons': ['sword', 'axe', 'mace', 'spear', 'bow', 'crossbow'], 'name': 'Ranger', 'age': '20', 'armor': 'leather', 'weapon': 'bow', 'class': 'ranger', 'mantra': 'I am the hunter', 'strength': '10', 'items': ['bow', 'dagger', 'dagger']}

出力結果

そのため必要なkey-valueのみを取得して、json.dumps()を行えばJSONに変換が可能です。

import json

# out.variables()から全ての変数の辞書を取得します
full_variables = out.variables()

# 必要なキーを指定します
keys_of_interest = ['description', 'valid_weapons', 'name', 'age', 'armor', 'weapon', 'class', 'mantra', 'strength', 'items']
# 必要なキーのみを含む新しい辞書を作成します
filtered_variables = {key: full_variables[key] for key in keys_of_interest}

# フィルター済みの辞書をJSONにシリアライズします
json_data = json.dumps(filtered_variables)
print(json_data)

{"description": "A quick and nimble fighter.", "valid_weapons": ["sword", "axe", "mace", "spear", "bow", "crossbow"], "name": "Ranger", "age": "20", "armor": "leather", "weapon": "bow", "class": "ranger", "mantra": "I am the hunter", "strength": "10", "items": ["bow", "dagger", "dagger"]}

出力結果

後工程で必要なJSONデータをカスタマイズできるのは、かなり便利だと感じます。


詰まったところ

公式notebookを実行すると、JSONシリアライズ部分で以下のエラーが発生します。

# ...that we could also use to generate compressed JSON
import json
json.dumps(out.variables())

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-7-b38e73ee7501> in <cell line: 3>()
      1 # ...that we could also use to generate compressed JSON
      2 import json
----> 3 json.dumps(out.variables())

3 frames
/usr/lib/python3.10/json/encoder.py in default(self, o)
    177 
    178         """
--> 179         raise TypeError(f'Object of type {o.__class__.__name__} '
    180                         f'is not JSON serializable')
    181 

TypeError: Object of type Transformers is not JSON serializable

原因としてはout.variables()にTransformersオブジェクトを値とした`llm`キーが存在しており、out.variables()をそのままJSON変換にできないことが原因でした。

ただよくよく考えてみると、out.variables()はさまざまな変数が格納されてきて、全ての変数の値をjson化したいケースは稀だろうなと思います。

おそらく今回共有した上記プログラムのような形で、後処理の工程で対応するのが良さそうだなと現時点では思っています。


本当にJSONデータの生成は失敗しないのか?

ここについてはまだきちんと深掘りして調査できていないのですが、おそらくLLMによって末尾文字列が適切に生成されない場合、失敗することがあるのではないかと思っています。

# プロンプトを定義
program = guidance("""The following is a character profile for an RPG game in JSON format.
```json
{
    "description": "{{description}}",
    "name": "{{gen 'name'}}",
    "age": {{gen 'age' pattern='[0-9]+' stop=','}},
    "armor": "{{#select 'armor'}}leather{{or}}chainmail{{or}}plate{{/select}}",
    "weapon": "{{select 'weapon' options=valid_weapons}}",
    "class": "{{gen 'class'}}",
    "mantra": "{{gen 'mantra'}}",
    "strength": {{gen 'strength' pattern='[0-9]+' stop=','}},
    "items": [{{#geneach 'items' num_iterations=3}}
        "{{gen 'this'}}",{{/geneach}}
    ]
}```""")

例えば、生成される文字列にダブルクオートやカンマが含まれない場合(主にはLLMの性能が弱い場合)には、文字列が生成され続けてしまうとかで、構文エラーとは別のエラーによって、失敗するケースは考えられそうです。

GuidanceのCompletion Generateの内部処理については、後日どこかで調査する予定です。


所感

JSON以外のさまざまなフォーマットにも今回の方法は応用ができそうです。これまで開発してきたLLMアプリケーションにすぐ取り入れるイメージが湧きました。かなり実用的な機能なのではないかなと思います。

Chat Completion形式のLLMだと、これらを利用することは現状できなさそうなので、利用する際は`text-davinci-003`やその他のOSS LLMsあたりでしょうか。

これまでは深く考えずに`gpt-3.5-turbo`や`gpt-4`を利用してきましたが、どのLLMモデルを用いるかの判断軸として浮上してきそうですね。


この記事が気に入ったらサポートをしてみませんか?