👻

OpenAI APIの推論結果を必ずJSONにする(感情認識)

2024/04/28に公開

はじめに

今回私が入社した会社の新卒研修で、Webアプリを作成しました。
その際に、OpenAIのAPIを使用したのですが、アプリケーション連携をする場合に出力の形式が固定されないというのが少し厄介でした。そんな中得たTipsを簡単に紹介します。
また、今回紹介するプロンプトは感情認識用のものになるので、そちらで困っている人も参考になるかと思います。

JSONモード

一言で言えば、OpenAIに入っているLLMモデルからの返答をJSON形式で返ってくることを保証させるモードです。
試せばわかるんですが、JSONモードがない場合、LLMモデルが話しすぎたりして欲しい情報をパースしてくるのが難しくなります。
このようなエラーを防ぐために、response_format{"type": "json_pbject"}に設定することでJSONにパースされる文字列のみを生成するように制約させることができます。

詳しくは、以下の公式のドキュメンテーションを参照してみてください。
https://platform.openai.com/docs/guides/text-generation/json-mode

余談ですが、Function callingでもJSON型で出力を固定できるみたいです。
https://note.com/daybreak_diary/n/nc76ea7b27cc1
ただ、Function callingだと、コードを少し多めに書かなきゃいけない他、多少精度が悪くなるみたい?
https://zenn.dev/microsoft/articles/azure-openai-function-calling-json-mode

使用技術

実装!

環境変数設定

Pythonに限らずプログラミング言語から「OpenAI API」をリヨうするには、「API Key」が必要です。Envファイルに認証のためのシークレットキーをおけば簡OpenAIのAPIは簡単に使えます。

"OPENAI_API_KEY": "<Your_OpenAI_API_Key>"

テキスト生成のコード

クラスを2つに分けて若干簡潔にかけていない感あるんですが、他の生成用のクラスも作成していたので、こんな感じでコードを書きました。
上のTextGenerationクラスのresponse_formatが例のそれです。これをいれることでJSON型で確実に文字が生成されます。嬉しい。。。

from typing import Any
from openai import OpenAI
from .utils import load_text_from_file, replace_text

def load_text_from_file(file_path):
    try:
        with open(file_path, 'r', encoding='utf-8') as file:
            text = file.read()
        return text 
    except Exception as e:
        print(f"Failed to load text from {file_path}: {e}")
        return None
    
def replace_text(text, replace_holder, value):
    
    try:
        updated_text = text.replace(replace_holder, str(value))
        return updated_text
    
    except Exception as e:
        print(f"Failed to replace {replace_holder} with {value} in text: {e}")
        return None

class TextGeneration:
    """
    A class for generating text using the OpenAI GPT model.

    Args:
        system_content (str): The initial system content to provide context for text generation.

    Attributes:
        client: An instance of the OpenAI client.
        system_content (str): The initial system content.

    Methods:
        __call__: Generates text based on user input.

    """

    def __init__(self, system_content) -> None:
        self.client = OpenAI()
        self.system_content = system_content

    def __call__(self, user_content: str): 
        """
        Generates text based on user input.

        Args:
            user_content (str): The user's input content.

        Returns:
            str: The generated text.

        """
        try: 
            completion = self.client.chat.completions.create(
                model="gpt-4-turbo",
                messages=[
                    {"role": "system", "content": self.system_content},
                    {"role": "user", "content": user_content},
                ],
                response_format={"type": "json_object"},
                temperature=2,
                max_tokens=4000,
                top_p=0.0,
                frequency_penalty=0,
                presence_penalty=0,
                stop=None,
            )

            return completion.choices[0].message.content

        except Exception as e:
            print(f"Failed to send text to OpenAI: {e}")
            return None


templates_folder = "/usr/src/app/gpt/prompt_templates"


class EmotionRecognizer:
    """
    A class that represents an emotion recognizer.

    Attributes:
        system_content (str): The content loaded from the system emotion recognition template file.
        user_content (str): The content loaded from the user emotion recognition template file.
        session (TextGeneration): An instance of the TextGeneration class for generating responses.

    Methods:
        __call__(text): Generates a response based on the given input text.

    """

    def __init__(self) -> None:
        """
        Initializes an EmotionRecognizer object.

        Loads the system and user emotion recognition template files,
        and initializes a TextGeneration session.

        """

        self.system_content = load_text_from_file(f'{templates_folder}/system_emotion_recognition.txt')
        self.user_content = load_text_from_file(f"{templates_folder}/user_emotion_recognition.txt")

        self.session = TextGeneration(system_content=self.system_content)

    def __call__(self, text) -> str:
        """
        Generates a response based on the given input text.

        Replaces the "{text}" placeholder in the user content with the input text,
        generates a response using the TextGeneration session, and returns the response.

        Args:
            text (str): The input text for emotion recognition.

        Returns:
            str: The generated response.

        """

        updated_user_content = replace_text(self.user_content, "{text}", text)

        response = self.session(updated_user_content)

        return response

システム側のプロンプト

感情認識用のシステム側のプロンプトは、以下のように構成しました。JSONモードにする際に、以下のように入力に対するJSONファイルの成果物の例のような文字(JSONという単語が入ることが必須)を入れないとエラーが起こります。

感情認識に関しては、Few Shot Promptingで今回構成しているのですが、この出力例は、以下の記事を参考にしました。
https://www.linkedin.com/pulse/gpt-language-model-prompt-emotion-personality-analysis-reuven-cohen

あと余談ですが、感情の分類に関しての研究は、Paulさんがとてつもなく成果を出しているようで、彼の論文を参考にするのも手かもしれません。
https://www.paulekman.com/about/paul-ekman/

# このコンテンツの前提条件
- あなたは、感情、人格特性、および臨床心理特性のテキスト分類モデルです。
- あなたは、感情、人格特性、及び臨床心理特性に基づいて入力テキストを分類します。

# このコンテンツの概要
- このプロンプトは、テキストの入力を感情、性格特性、および臨床心理特性に基づいて分類するために設計されています。
- 分析を希望するテキストをうけとり、以下の形式で情報を取得してください:感情、感情の確信度、感情の強度、性格特性、性格特性の確信度、臨床特性、および臨床特性の確信度。適切な分類と確信度を通じて、入力されたテキストの心理的および感情的内容を正確に理解しましょう。
- 感情は通常、特定の出来事に対する人々の反応として理解されます。Paul Ekmanによれば、基本的な感情としては「Happiness」、「Anger」、「Surprise」、「Neutral」、「Sadness」の5つのみで分類されます。

# 入力に対するJSONファイルの成果物の例
入力: "I love my new job, it's fantastic!" 
出力: {
    "passion": "I love my new job, it's fantastic!" ,
    "emotion":"Happiness", 
    "confidence_emotion":0.95, 
    "intensity_emotion":0.90, 
    "personality_trait":"optimistic", 
    "confidence_trait":0.92, 
    "clinical_trait":"non-depressive", 
    "confidence_clinical":0.97
    }

入力: "I can't believe they messed up my order again!" 
出力: {
    "passion": "I can't believe they messed up my order again!" ,
    "emotion":"Anger", 
    "confidence_emotion":0.92, 
    "intensity_emotion":0.85, 
    "personality_trait":"impatient", 
    "confidence_trait":0.90, 
    "clinical_trait":"non-anxious", 
    "confidence_clinical":0.95
    }

入力: "The food was great, but the service was terrible." 
出力: {
    "passion": "The food was great, but the service was terrible." ,
    "emotion":"Sadness", 
    "confidence_emotion":0.88, 
    "intensity_emotion":0.75, 
    "personality_trait":"critical_thinker", 
    "confidence_trait":0.87, 
    "clinical_trait":"non-anxious", 
    "confidence_clinical":0.94
    }

入力: "This product exceeded my expectations!" 
出力: {
    "passion": "This product exceeded my expectations!",
    "emotion":"Surprise", 
    "confidence_emotion":0.97, 
    "intensity_emotion":0.80, 
    "personality_trait":"open-minded", 
    "confidence_trait":0.95, 
    "clinical_trait":"non-depressive", 
    "confidence_clinical":0.98
    }

入力: "I'm so frustrated with the slow internet connection!" 
出力: {
    "passion": "This product exceeded my expectations!",
    "emotion":"Anger", 
    "confidence_emotion":0.93, 
    "intensity_emotion":0.88, 
    "personality_trait":"impatient", 
    "confidence_trait":0.89, 
    "clinical_trait":"non-anxious", 
    "confidence_clinical":0.96
    }

入力: "Today is a sunny day. Everything is going as planned." 
出力: {
    "passion": "Today is a sunny day. Everything is going as planned.",
    "emotion":"Neutral", 
    "confidence_emotion":0.85, 
    "intensity_emotion":0.70, 
    "personality_trait":"calm", 
    "confidence_trait":0.88, 
    "clinical_trait":"non-anxious", 
    "confidence_clinical":0.90
    }

入力: "I received some disappointing news today." 
出力: {
    "passion": "I received some disappointing news today.",
    "emotion":"Sadness", 
    "confidence_emotion":0.90, 
    "intensity_emotion":0.85, 
    "personality_trait":"resilient", 
    "confidence_trait":0.87, 
    "clinical_trait":"non-depressive", 
    "confidence_clinical":0.92
    }

ユーザー側のプロンプト

以下の{text}を置き換えることで感情を認識します。

以下があなたに、感情分類してほしいテキストです。

# テキスト
- 感情分類してほしいテキスト: {text}

出力結果

入力をやる気満々だよ!!!とすると以下:

{
    "passion": "やる気満々だよ!!!",
    "emotion": "Happiness",
    "confidence_emotion": 0.95,
    "intensity_emotion": 0.93,
    "personality_trait": "energetic",
    "confidence_trait": 0.91,
    "clinical_trait": "non-depressive",
    "confidence_clinical": 0.96
}

入力をちょっとやる気でないなぁとすると以下:

{
    "passion": "ちょっと今日やる気でないなぁ",
    "emotion": "Sadness",
    "confidence_emotion": 0.88,
    "intensity_emotion": 0.65,
    "personality_trait": "low_energy",
    "confidence_trait": 0.85,
    "clinical_trait": "non-depressive",
    "confidence_clinical": 0.90
}

になります!
とてもおもしろいですね。JSON形式で出力できたのと、感情認識もなんとなく精度高そうな結果が得られました。

感想

アプリケーションとの連携を考えている方でこれを知らない人は少し参考になったかなと思います。
感情認識もまあまあいい感じ?と思いませんか?

Discussion