OpenAIのJSON Modeでテキストを一定のフォーマットに整形してみた

2023.11.11

こんちには。

データアナリティクス事業本部 インテグレーション部 機械学習チームの中村です。

今回は先日発表のあったJSON Modeで、ある自然文(テキスト)をうまく一定のフォーマットに整形することができないか試してみました。

機能の概要

GPT-4 TurboとGPT-3.5 Turboの最新版は、フォーマットの指示により厳密に従ってくれるようにパフォーマンスが改善し、それとは別に、モデルが有効なJSONで応答することを保証する新しいJSONモードもサポートしました。

これらはgpt-4-1106-previewまたはgpt-3.5-turbo-1106を指定することで使用可能です。

response_formatを{type:"json_object"}に設定すると、JSONモードが有効にできます。

前提として以下に気を付ける必要があります。

  • JSONモードを使用するには、システムメッセージでモデルにJSONを生成するように指示する必要あり
    • システムメッセージに"JSON"という文字列がない場合、APIはエラーとなる
  • finish_reasonが lengthの場合、モデルが返すメッセージは部分的なものになる可能性がある
  • JSONモードは、出力が特定のスキーマにマッチすることを保証するものではなく、JSONとしてエラーなくパースされることだけを保証するもの

こちらを使ってとあるテキストを整形することを試してみます。

試してみた

整形したいデータ

各人がカレンダーなどに入力している今日の勤務予定などをイメージしてください。

例えば以下のようなテキストです。

日比谷出社(中村)
日比谷出社 (中村)
日比谷 出社 (中村)
日比谷出社 (中村)
日比谷出社 (中村)
有休(中村
中村は今日大阪出張です
(家庭の事情で)リモート予定(中村)

自由入力欄にしていると、みなさん思い思いの形式で入力してしまうケースあるあるですよね。

これをうまいこと整形していきましょう。

環境の前提

GPTモデルを使ったJSONパーサーを定義

以下のようなプロンプトを持ったGPTモデルへのクエリで、JSONパーサーを実装してみました。

system_prompt = """
あなたはテキストを整形するアシスタントです。
与えられた文字列に対して、名前と出社ステータスと場所をJSON形式でパースしてください。
JSONのキーはname, status, locationとしてください。

nameは人名とします。
statusは出社、リモート、出張、休日の3種類から選んでください。
locationは、statusが出社または出張の場合のみパースして下さい。

関係のない文字列については空欄のJSON形式を返してください。
"""
def json_parser(query: str):

    import json
    from openai import OpenAI

    client = OpenAI()

    response = client.chat.completions.create(
        model="gpt-3.5-turbo-1106",
        temperature=0.0,
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": query},
        ],
        response_format={"type": "json_object"}
    )

    if response.choices[0].message.content is None:
        return {}
    else:
        return json.loads(response.choices[0].message.content)

response_format={"type": "json_object"}がJSONモードが有効であるために必要な条件です。

JSONは文字列になっているので、json.loadsでPythonのdict型に変換してreturnします。

よくわからんリクエストになっている場合は{}returnします。

整形を動かしてみた

以下のようなデータに対して実行します。

data = """
日比谷出社(中村)
日比谷出社 (中村)
日比谷 出社 (中村)
日比谷出社 (中村)
日比谷出社 (中村)
有休(中村
中村は今日大阪出張です
(家庭の事情で)リモート予定(中村)
""".strip("\n").split("\n")

import polars as pl

df = pl.DataFrame([{"text": d, **json_parser(d)} for d in data])

後半のpolarsは表示用ですのでpandasとかなんでも結構です。必要に応じてpip等でインストールされてください。

結果は以下のようになりました。

print(df)

# shape: (8, 4)
# ┌───────────────────────────────────┬──────┬──────────┬────────────────┐
# │ text                              ┆ name ┆ status   ┆ location       │
# │ ---                               ┆ ---  ┆ ---      ┆ ---            │
# │ str                               ┆ str  ┆ str      ┆ str            │
# ╞═══════════════════════════════════╪══════╪══════════╪════════════════╡
# │ 日比谷出社(中村)                  ┆ 中村 ┆ 出社     ┆ 日比谷         │
# │ 日比谷出社 (中村)                 ┆ 中村 ┆ 出社     ┆ 日比谷         │
# │ 日比谷 出社 (中村)                ┆ 中村 ┆ 出社     ┆ 日比谷         │
# │ 日比谷出社 (中村)               ┆ 中村 ┆ 出社     ┆ 日比谷         │
# │ 日比谷出社 (中村)              ┆ 中村 ┆ 出社     ┆ 日比谷         │
# │ 有休(中村                        ┆ 中村 ┆          ┆                │
# │ 中村は今日大阪出張です            ┆ 中村 ┆ 出張     ┆ 大阪           │
# │ (家庭の事情で)リモート予定(中  ┆ 中村 ┆ リモート ┆                │
# │ 村)                              ┆      ┆          ┆                │
# └───────────────────────────────────┴──────┴──────────┴────────────────┘

良い感じに変換できていそうです。

まとめ

いかがでしたでしょうか。JSONモードを使わずとも実装できる可能性はありますが、後処理の整形や変換処理ができなかった場合の例外処理は煩雑になってしまいがちだと思います。

今回それらを簡単に実現できるようになったのは大きいですね。

冒頭にもかきましたが、system_promptでキーを指定しても、JSONモードはそのキーを含むことは保証してくれません(JSONとしてパースできることのみを保証します)。この点はご注意ください。

本記事がご参考になれば幸いです。