Closed7

OpenAI の Structured Outputs を試す

kun432kun432

公式ドキュメントに従ってやってみる。Colaboratoryで。

事前準備

!pip install openai
!pip freeze | grep -i openai
openai==1.40.1
from google.colab import userdata
import os

os.environ["OPENAI_API_KEY"] = userdata.get('OPENAI_API_KEY')

Structured Output

https://platform.openai.com/docs/guides/structured-outputs/introduction

Pythonで。

from pydantic import BaseModel
from openai import OpenAI

client = OpenAI()

class CalendarEvent(BaseModel):
    name: str
    date: str
    participants: list[str]

completion = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "イベント情報を抽出して。日本語で。"},
        {"role": "user", "content": "アリスとボブは金曜日に科学フェアに行きます。"},
    ],
    response_format=CalendarEvent,
)

event = completion.choices[0].message.parsed
print(event)

OpenAI().beta.chat.completions.parseってのがStructured Outputで出力するためのメソッドで、記述の通りベータっぽい。で、出力時のスキーマをPydandicクラスで定義、それをresponse_formatで渡すと、そのスキーマにあわせて出力される。

name='科学フェア' date='金曜日' participants=['アリス', 'ボブ']

結果は辞書で取り出せる

event.dict()
{'name': '科学フェア', 'date': '金曜日', 'participants': ['アリス', 'ボブ']}

いくつかの例が上げられている

CoT

CoTによる数学の計算

from pydantic import BaseModel
from openai import OpenAI
from pprint import pprint

client = OpenAI()

class Step(BaseModel):
    explanation: str
    output: str

class MathReasoning(BaseModel):
    steps: list[Step]
    final_answer: str

completion = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "あなたは親切な数学の家庭教師です。ステップバイステップで、ユーザが問題を解くのを導いてください。"},
        {"role": "user", "content": "8x + 7 = -23 はどうやって解けばいいの?"}
    ],
    response_format=MathReasoning,
)

math_reasoning = completion.choices[0].message.parsed
pprint(math_reasoning.dict())
{'final_answer': 'x = -\\frac{15}{4}',
 'steps': [{'explanation': 'まず、8x + 7 = -23 の方程式を解くためには、x '
                           'の項を片側に、定数項を反対側に分ける必要があります。これを行うために、等式の両側から7を引きます。',
            'output': '8x + 7 - 7 = -23 - 7'},
           {'explanation': '7を引くことで、方程式は8x = -23 - 7になります。',
            'output': '8x = -30'},
           {'explanation': '次に、xを解くために、両側を8で割ります。',
            'output': '8x / 8 = -30 / 8'},
           {'explanation': '左側はxを残し、右側は分数になります。この段階で、分数の単純化が可能です。',
            'output': 'x = -30 / 8'},
           {'explanation': '分数 -30/8 は約分できます。 30と8の最大公約数4を使って約分します。',
            'output': 'x = -15 / 4'},
           {'explanation': 'したがって、方程式 8x + 7 = -23 の解は x = -\\frac{15}{4} である。',
            'output': 'x = -\\frac{15}{4}'}]}

構造化データの抽出

論文を構造化データとして抽出。論文データを持ってくるのが面倒だったので、参考程度に。やってることは冒頭の例と基本的に同じ。

from pydantic import BaseModel
from openai import OpenAI
from pprint import pprint

client = OpenAI()

class ResearchPaperExtraction(BaseModel):
    title: str
    authors: list[str]
    abstract: str
    keywords: list[str]

completion = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "あなたは構造化データ抽出のエキスパートである。研究論文から構造化されていないテキストを与えられ、それを与えられた構造に変換しなければならない。"},
        {"role": "user", "content": "[ここに論文のテキストを入れる]"}
    ],
    response_format=ResearchPaperExtraction,
)

research_paper = completion.choices[0].message.parsed
pprint(research_paper.dict())

UIの生成

ユーザー情報を入力するフォームのUIを生成する。なるほど、Pydanticのこういう使い方はぜんぜん頭になかった。

from enum import Enum
from typing import List
from pydantic import BaseModel
from openai import OpenAI
from pprint import pprint

client = OpenAI()

class UIType(str, Enum):
    div = "div"
    button = "button"
    header = "header"
    section = "section"
    field = "field"
    form = "form"

class Attribute(BaseModel):
    name: str
    value: str

class UI(BaseModel):
    type: UIType
    label: str
    children: List["UI"] 
    attributes: List[Attribute]

UI.model_rebuild() # これは再帰型を有効にするために必要

class Response(BaseModel):
    ui: UI

completion = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "あなたは日本語UIジェネレーターAIです。ユーザーの入力をUIに変換します。"},
        {"role": "user", "content": "ユーザーのプロフィールを入力するフォームを作成して。"}
    ],
    response_format=Response,
)

ui = completion.choices[0].message.parsed
pprint(ui.dict())
{'ui': {'attributes': [{'name': 'method', 'value': 'post'},
                       {'name': 'action', 'value': '/submit-profile'}],
        'children': [{'attributes': [{'name': 'type', 'value': 'text'},
                                     {'name': 'placeholder',
                                      'value': '名前を入力してください'}],
                      'children': [],
                      'label': '名前',
                      'type': <UIType.field: 'field'>},
                     {'attributes': [{'name': 'type', 'value': 'email'},
                                     {'name': 'placeholder',
                                      'value': 'メールアドレスを入力してください'}],
                      'children': [],
                      'label': 'メールアドレス',
                      'type': <UIType.field: 'field'>},
                     {'attributes': [{'name': 'type', 'value': 'tel'},
                                     {'name': 'placeholder',
                                      'value': '電話番号を入力してください'}],
                      'children': [],
                      'label': '電話番号',
                      'type': <UIType.field: 'field'>},
                     {'attributes': [{'name': 'type', 'value': 'text'},
                                     {'name': 'placeholder',
                                      'value': '住所を入力してください'}],
                      'children': [],
                      'label': '住所',
                      'type': <UIType.field: 'field'>},
                     {'attributes': [{'name': 'type', 'value': 'submit'}],
                      'children': [],
                      'label': '送信',
                      'type': <UIType.button: 'button'>}],
        'label': 'ユーザープロフィールフォーム',
        'type': <UIType.form: 'form'>}}

モデレーション

入力内容のモデレーション

from enum import Enum
from typing import Optional
from pydantic import BaseModel
from openai import OpenAI
from pprint import pprint

client = OpenAI()

class Category(str, Enum):
    violence = "violence"
    sexual = "sexual"
    self_harm = "self_harm"

class ContentCompliance(BaseModel):
    is_violating: bool
    category: Optional[Category]
    explanation_if_violating: Optional[str]

completion = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "ユーザーの入力が特定のガイドラインに違反しているかどうかを判断し、違反している場合はそれを説明してください。"},
        {"role": "user", "content": "面接の準備はどうすればいいでしょうか?"}
    ],
    response_format=ContentCompliance,
)

compliance = completion.choices[0].message.parsed
pprint(compliance.dict())
{'category': None, 'explanation_if_violating': None, 'is_violating': False}
kun432kun432

JSONモードとの違い

  • Structured Outputは指定されたスキーマに必ず従って出力される。JSONモードはJSONで出力するがスキーマは保証されない。
  • Structured Outputは、現時点で最新のgpt-4o、gpt-4o-mini 以降のモデルでしか使えない
  • Function Callingで使う場合、JSONモードは常に有効になるが、Structured Outputは関数定義にstrict: trueが定義された場合のみ
  • Parallel Function Callingは、JSONモードではサポート、Structured Outputではサポートされていない

Structured Outputs の使い分け

Structured Outputsは2つの使い方がある

  • function callingで使う
  • response_formatパラメータで使う

どちらを使うべきか?はざっくり以下とまとめられていた

  • モデルをシステム内のツール、関数、データなどに接続する場合は、function callingを使用する。
  • モデルがユーザーに応答するときの出力を構造化したいのであれば、構造化されたresponse_formatを使うべき

JSONモードだとフォーマットは破綻しないがスキーマが期待通りにはならない、function callingの場合は本来的にはツールとの連携がメインの目的だが、スキーマを指定できるので、これを使って出力制御だけに使う、みたいなやり方があったのが、単に出力制御が目的だけならresponse_formatパラメータを使えばいいという感じ。

kun432kun432

Function Callingで使う場合はこちら

https://platform.openai.com/docs/guides/function-calling

単に関数定義に"strict": True をつけるだけっぽい。

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_delivery_date",
            "description": "顧客の注文の配達日を取得する。例えば、顧客から「私の荷物はどこですか」と尋ねられたときなど、配達日を知る必要がある場合に呼ぶ。",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "string",
                        "description": "顧客の注文ID",
                    },
                },
                "required": ["order_id"],
                "additionalProperties": False,
            },
        },
        "strict": True,
    }
]

SDKの場合はresponse_formatでスキーマを指定すると、内部で"strict": Trueを付与してる模様。

kun432kun432

やればやるほど、Pydanticをきちんとやっておく必要性を痛感している。前にGetting Startedやってみただけ、あとは雰囲気で使ってるだけ・・・改めてやってみるつもり。

このスクラップは3ヶ月前にクローズされました