🗂

Phi-4のプロンプトで合成Preferenceデータセットを作る

2024/12/19に公開

気力とネタが尽きて1週間空いてしまった1人ローカルLLMアドベントカレンダーの12日目です。

https://qiita.com/advent-calendar/2024/local-llm

要約

  • Phi-4のDPO判定プロンプトを使ってPreferenceデータセットの判定を行った

目的

DPOなどに用いるPreferenceデータセットをLLMで生成する際に課題になるのがchosenとrejectedを判定する部分になるのではないかと思います。

この判定部分をローカルモデルにやらせようと試行錯誤していたもののうまくいかず断念していたのですが、Phi-4のテクニカルレポートに書かれていたDPO判定用のプロンプトが良さそうだったので試していこうと思います。

実施内容

使用する応答

https://zenn.dev/kendama/articles/5b6b5f12faa9c2

こちらの記事を書くときに生成した応答を使いまわします。

  • Qwen/Qwen2.5-32B-Instruct-AWQ
  • mistralai/Mistral-Nemo-Instruct-2407

の2つのモデルで生成したものを対象として、7,868件の評価を行っていきます。

評価

プロンプト

参考までに、Phi-4のレポートが出るまで評価には以下のプロンプトを使用していました。

これまで使ってきたプロンプト
以下のinstructionに対して、response1とresponse2のどちらがユーザーにとって適切な応答をしているか判定してください。
なお、「適切」とは、ユーザーからのinstructionに忠実に従い、提供する情報に過不足がなく、正確な情報を提供できていることを指します。
- response1がより適切である場合には1を、response2がより適切である場合には2を出力してください。
- まずはresponse1とresponse2を比較した内容を100語程度で「Analysis: 」の後にまとめ、最終的に判定結果を「Winner: 」の後に出力すること
- 前置きはせず、フォーマットに従って判定結果のみを取得すること

<instruction>
INSTRUCTION
</instruction>

<response1>
RESPONSE1
</response1>

<response2>
RESPONSE2
</response2>

<format>
Analysis:
Winner: 1 or 2
</format>

軽い分析をした後にどちらの応答が良かったかを出力させるものなのですが、先に書いた応答を高く評価するという位置バイアスがかなり強く効いていました。そのため、ほとんどの場合で応答の順番を入れ替えて評価すると結果が変わり、評価が適切に行えているとは言えない状況でした。

次に、Phi-4のプロンプトを翻訳し、少し手を加えたプロンプトは以下のものになります。

Phi-4を参考にしたプロンプト
あなたのタスクは、AIアシスタントが返した以下の回答のうち、どちらがより適切かを判断することです。
# Conversation
USER: {instruction}

# Replies
## Assistant1

{response1}

## Assistant2
{response2}

# Guideline
以下のJSON形式で出力を生成してください(コメントは不要で、エスケープ文字は正しく使用してください)。
{{
    "faults": {{
        "Assistant1": (string) Assistant1の回答の問題点をすべて列挙します。各問題について、関連資料の理解不足、論理エラー、事実エラー、表現上の問題、言語の不一致のいずれに起因するかを判断します。回答が完璧であれば「none」と記入します。質問で説明の詳細レベルが指定されていない場合は、回答が詳細すぎたり簡潔すぎたりしても減点しないでください。
        "Assistant2": ... ...
    }} ,
    "faults_discussion": (string) 各アシスタントの一般的な長所と短所について説明してください。回答のスタイル、正確さ、詳細さのレベルにおける主な違いは何ですか?,
    "accuracy": {{
        "Assistant1": (1-5) 精度の観点からAssistant1をどのように評価しますか?
        ...
    }},
    "style": {{
        "Assistant1": (1-5) 応答のスタイルの観点からAssistant1をどのように評価しますか?
        ...
    }},
    "detail": {{
        "Assistant1": (1-5) 詳細さの観点からAssistant1をどのように評価しますか?
        ...
    }}
}}

このプロンプトでは最初にそれぞれの応答の問題点を挙げ、次にそれぞれの長所・短所を比べます。その後、正確性、スタイル、詳細さの3つの観点について5点満点での採点を行うようになっています。

評価の実施

評価者にはQwen/Qwen2.5-32B-Instructを使用します。本来は応答の生成に使ったモデルとは別のものを使うべきかもしれません...

要求する出力がJSON形式なので、生成にはvllmのstructured outputを使いました。これによってJSON出力が安定するため、生成後に余計な処理を加えずに結果を取得することができます。

classの設定
class Score(BaseModel):
  Assistant1: int
  Assistant2: int

class Reason(BaseModel):
  Assistant1: str
  Assistant2: str

class DPOJudgement(BaseModel):
  faults: Reason
  faults_discussion: str
  accuracy: Score
  style: Score
  detail: Score
評価生成の関数
def get_scores(dic):
    acc1 = dic["accuracy"]["Assistant1"]
    acc2 = dic["accuracy"]["Assistant2"]
    style1 = dic["style"]["Assistant1"]
    style2 = dic["style"]["Assistant2"]
    detail1 = dic["detail"]["Assistant1"]
    detail2 = dic["detail"]["Assistant2"]
    score1 = acc1 + style1 + detail1
    score2 = acc2 + style2 + detail2
    return score1, score2

def judge(inst, res1, res2):
    client = OpenAI(
        api_key="EMPTY",
        base_url="http://localhost:8000/v1"
    )
    
    messages1 = generate_planner_messages(inst, res1, res2)
    messages2 = generate_planner_messages(inst, res2, res1)
    
    response1 = client.chat.completions.create(
        messages=messages1,
        model=model_name,
        extra_body={"guided_json": json_schema},
    )

    response2 = client.chat.completions.create(
        messages=messages2,
        model=model_name,
        extra_body={"guided_json": json_schema},
    )

    json1 = json.loads(response1.choices[0].message.content)
    json2 = json.loads(response2.choices[0].message.content)

    response1_score1, response2_score1 = get_scores(json1)
    response2_score2, response1_score2 = get_scores(json2)

    response1_score = response1_score1 + response1_score2
    response2_score = response2_score1 + response2_score2

    if response1_score > response2_score:
        result = "assistant1"
    elif response1_score == response2_score:
        result = "tie"
    else:
        result = "assistant2"
    
    return {"judge1": json1, "judge2": json2, "result": result}

評価については、位置バイアスを考慮して、応答の順番を入れ替えて計2回の判定を行っています。それぞれの判定でのaccuracy、style、detailの3項目をすべて足した値を応答のスコアとして、スコアが高い方をchosen、低い方をrejectedとしました。

これについては、2回の判定のどちらにも勝っていた応答をchosenとしたほうが質の高いpreferenceデータが得られるかもしれません。

結果

今回の検証で作成したデータはこちらになります。

https://huggingface.co/datasets/Kendamarron/Synthetic-Preference-ja-Qwen2.5-32B-Mistral-Nemo

chosenに選ばれた勝率は以下のようになりました。

モデル 勝率
Qwen/Qwen2.5-32B-Instruct-AWQ 67.3%
mistralai/Mistral-Nemo-Instruct-2407 22.9%
引き分け 9.8%

モデルの日本語性能と評価者にしていることからQwenが高いのは当たり前ですが、思ったよりMistralの勝率が高かったです。

実際の出力を見てもMistralをchosenと選んだレコードを見てみると、判定結果にそこまで違和感はなく、それなりにしっかりと判定されている印象でした。

まとめ

今回はPhi-4のプロンプトを使ってPreferenceデータセットの生成をしてみました。

応答の生成に使うモデルの選定を間違えた感があるので、そのうち再挑戦しようと思います。

Discussion