こしあん
2024-02-05

LLMに検索エンジンの検索ワードを生成させる(AI王データセット)


514{icon} {views}


検索エンジンの結果を利用してRAGをするために、LLMを使って検索ワードを自動生成させたい。AI王データセットの中から、GPTの知識だけでは解けない問題を選択し、定量評価することで、プロンプトエンジニアリングやハイパーパラメータの選択がやりやすくなった。

目的

  • Bingチャットのように、検索エンジンの結果を使ってRAGをしたい
  • 検索エンジンに与えるための検索ワードをどのように生成するかというのが主題
  • LLMに検索ワードを生成させるのだが、プロンプトエンジニアリングを下流タスクのデータセットを使って定量的に行う
    • 巷に出ているプロンプトエンジニアリングは、経験則になってしまっており、科学的とは言い難い
  • 下流タスクのデータセットとは、前回作成したAI王からのデータセットで、GPT-4が外部知識なしで解けなかったクイズ問題のデータセット「abc_01-12_hard」である

流れ

回答プロセスは以下の流れで行う。

  • 問題文が与えられる
  • 問題文に対するGoogleのワードをLLMで作成
  • Google検索を行い結果を習得
  • その結果を文脈として埋め込んで、LLMで回答を生成し、最終回答とする

今回特に「LLMによる検索ワードの生成」がポイントとなる。

具体例

  • 例えば、「江戸時代の「五街道」の中で、宿場の数が69と最も多かったのはどこでしょう?」という問題があったとする。
  • LLMが検索ワード(例:「五街道 宿場数 最大」)を生成する
  • Google検索の結果を習得する
  • この結果をもとにRAG行う。RAGの部分は以下のようなプロンプトになる。基本的には前回のを少しアレンジしただけ

RAGのシステムプロンプト

あなたはクイズの回答者です。以下の正誤判定基準に留意して、ユーザーから与えられた問題と文脈に応じてに返答してください。返答は問題の回答のみ教えて下さい
回答の際は、文脈(Context)を使用して、ユーザー入力の最後の質問(Question)に答えてください。

## ユーザーの入力
Context:
{ここに文脈が書かれます}


Question: {ここに与えられた問題が書かれます}
Helpful Answer:

## 正誤判定基準
【人名について】
・人名に関しての敬称・肩書(「○○さん」「○○選手」など)の有無は、正誤判定には関係ない。しかし、明らかに間違った敬称・肩書をつけた場合は誤答扱いにする。
・人名は原則として、芸名・ペンネーム・登録名など最も一般的に知られている名前、及びそれに準ずる知名度のもののみを正解とし、本名や別号など「知名度が低い」と正誤判定者が判断した解答は誤答の対象とする。前者の正解対象が回答に含まれていれば正解とする。
・東洋人名(日本・韓国・中国・台湾など、主に氏名表記に漢字が用いられる国)は、フルネームでの解答に限り正解とする。ただし、東洋人名でも、通称が固有名詞化した場合や、四股名はこの限りではない。
・西洋人名は、原則ファミリーネームのみで正解とする。ただし、明らかにフルネームを要求される問題の場合は例外とする。
・複合姓はファミリーネーム同様、区別の必要性がある場合のみ求め、原則一般的な呼称で正解とする。
・以上の人名に関するルールは、実在しないキャラクター等にも適用する。

【複数解答について】
・複数のものから一つだけを答える問題の場合、問題文のままの順序で全てを答えた場合は正解とするが、それ以外の場合、全てを解答するのは不正解とする。
・都道府県名と都市名の両方を答えた場合、原則都市名の方を答えたものとみなす。

【文章での解答について】
・歌詞や文章の一部を問う問題で、問われているもの以上を解答した場合は、たとえ歌詞・文章通りでも誤答の対象とすることがある。
・ただし、ことわざや慣用句など、全体で1つのイディオムとして認知されている言葉はこの限りではない。
・ことわざや慣用句のすべてを答えた際、問われているものは正解だが、問題文と一部が異なっていた場合は誤答の対象とすることがある。

RAGのユーザープロンプト

Context:
{context}


Question: {question}
Helpful Answer:

実験

  • GPT-4が外部知識なしで解けなかったクイズ問題のデータセット「abc_01-12_hard」に対し、Google検索の結果による文脈を埋め込み回答(RAG)させる
  • Google検索はGoogle CloudのCustom Search JSON APIを使用
    • トライアルの300ドルクレジットで間に合ったが、1日のコール回数のクォーターが10000しかないので注意
  • RAGでは検索の結果のみ使用し、個々のページにはアクセスしない。これは処理を簡単にするのと、検索エンジンが出したサイトの安全性を担保できないから(フィッシングサイトの場合もあるから)。
  • RAGの部分は何らかのEmbedding APIを使い、問題文との類似度を見て、情報の取捨選択やソートを行う
  • 実験を高速に回すため、3769問あるabc_01-12_hardから500問をランダムに選択し、評価データとする
  • 各種条件を変え、500問に対する正解率の変動を見る
  • 回答コード、推論コードは末尾参照

プロンプトエンジニアリング(+++)

Case1: 問題文をそのままGoogle検索に入れる

検索ワードの生成をLLMで行わず、問題文をそのままGoogle検索に入れた例

例:『江戸時代の「五街道」の中で、宿場の数が69と最も多かったのはどこでしょう?』という問題だったら、これをそのままGoogle検索に入れる

Case2: 検索ワード生成用のプロンプトをGPT-4 Turboに生成させる

検索ワード生成プロンプトをGPT-4に作成させる。古くから機械学習モデルはパターン認識をやっているため、こういうやり方はもともと得意。普段無意識にやっている検索ワード生成基準を言語化したいときにも有効。

各問題に対応するGoogle検索のワードを自分で10パターンほど、アノテーションし、GPT-4 Turboに問題と検索ワードを入力・出力として与える。

システムプロンプト

:今検索ワードを生成しようとしています。ユーザーから与えられた入出力に対応するような、ChatGPT向けのプロンプトを作成してください

ユーザープロンプト

・入力:(問題文1)
・出力:(問題文1に対応する、人手で作成した検索ワード)

・入力:(問題文2)
・出力:(問題文2に対応する、人手で作成した検索ワード)

・入力:(問題文3)
・出力:(問題文3に対応する、人手で作成した検索ワード)

: :

結果

特定の事物や概念に関する知識を問う「何というでしょう?」形式のクイズ質問に基づいて、検索ワードを抽出し生成してください。入力されたクイズの質問から、主要なキーワードやテーマを識別し、それに関する適切な出力として検索ワードをまとめてください。各検索ワードは目的の答えや情報を効率的に見つけるための主要要素を含むべきです。

ただし、後段のGoogle検索の処理を楽にするために、JSONモードの指定を追加する

特定の事物や概念に関する知識を問う「何というでしょう?」形式のクイズ質問に基づいて、検索ワードを抽出し生成してください。入力されたクイズの質問から、主要なキーワードやテーマを識別し、それに関する適切な出力として検索ワードをまとめてください。各検索ワードは目的の答えや情報を効率的に見つけるための主要要素を含むべきです。
出力はJSON形式で、search_wordsをキーとして、値はリスト形式で出してください。

Case3:手動で簡単なプロンプトを作る

人間が作るんだったらこんなのだろうという簡単なプロンプト

あなたは検索エンジンです。ユーザーから与えられた問題を回答にするための適切な検索ワードを生成してください。
出力はJSON形式で、search_wordsをキーとして、値はリスト形式で出してください。

Case4:手動で詳細なプロンプトを作る

出てきた検索ワードを見ながら、そこそこ人間がチューニングしたプロンプト。ベースはCase2のGPT-4が作成したものだが、「こうしたほうが答えやすいだろう」というチューニングが入っている。

以下の条件に基づいて、ユーザーの質問に対して、その回答を効率よく見つけるために検索エンジンに与える検索ワードを抽出し生成してください。

・入力された質問から、主要なキーワードやテーマを識別し、それに関する適切な出力として検索ワードをまとめてください。
・各検索ワードは目的の答えや情報を効率的に見つけるための主要要素を含むべきです。
・質問の中で英語を指定してきても、検索ワードは日本語で提示してください。
・検索ワードの候補を3つ出してください。検索エンジンに与えたときに最も回答を見つける可能性の高い検索ワードを1番目に出してください。
・出力はJSON形式で、search_wordsをキーとして、値はリスト形式で出してください。

定量評価

各ケースごと、正誤判定者のLLMを変えたときの結果は以下の通り

正解数(正誤判定GPT-3.5) ←正解率 正解数(正誤判定GPT-4) ←正解率
Case1 27 5.4% 29 5.8%
Case2 106 21.2% 135 27.0%
Case3 218 43.6% 270 54.0%
Case4 250 50.0% 320 64.0%

各種条件

  • ここでのGPT-3.5はgpt-3.5-turbo-1106、GPT-4はgpt-4-0125-previewを意味する
  • 検索ワード生成、RAGの生成はすべてGPT-3.5
  • Google検索は1回行い10件習得。うち10件の情報をRAGで使用。言語は日本語(lang_ja)を指定
  • Embedding APIはtext-embedding-3-small
  • 正誤判定者はGPT-4のほうが表記揺れを取りこぼしづらくなるため、「本当は正解だったのに不正解になっているケース」を取りこぼしにくい(前回参照)

考察

  • 検索エンジンのプロンプトエンジニアリングがかなり効く
  • 問題文を直接検索エンジンに入れるのは悪い(Case1)。これは今までの経験則通り
  • シンプルなプロンプトでも、人間が作ったもののほうがGPT-4よりも良い結果になる(Case2、3)。ただ、プロンプトの作成能力は人によって異なるので、必ずしも広く当てはまるとは限らない。
  • GPT-4に作らせたプロンプトは人間的に評価すると結構良いが、クイズの場合は、正解率の結果は伴わなかった(Case2)。もしかしたらプロンプトを自動作成するのは、まだGPTの能力的に限度があるのかもしれない。GPTの生成文をそのまま使うと、ユーザーが意図しているトンマナとの間に差がある。
  • GPT-4に作らせたプロンプトが微妙でも(Case2)、人間がそれをベースに改良すると最も良くなる(Case4)

定性評価

以下の3サンプルに対する検索ワードの生成結果をCase2-4で比較する。検索ワード生成はGPT-3.5。

  • Q1:日本料理で、「利休」といえばゴマを使った料理ですが、「吉野」といえばどんなものを使った料理でしょう?
  • Q2:安田大サーカス、よゐこ、ますだおかだなどが所属する芸能事務所はどこでしょう?
  • Q3:2011-12シーズン、日本フットサルリーグに参加していたクラブ数はいくつでしょう?
問題文 問題番号 検索ワード1 検索ワード2 検索ワード3 検索ワード4 検索ワード5
Case2 Q1 吉野 日本料理 吉野料理 吉野料理の材料
Q2 安田大サーカス よゐこ ますだおかだ 所属 芸能事務所
Q3 2011-12 シーズン 日本フットサルリーグ クラブ数
Case3 Q1 吉野料理 吉野料理 材料 吉野料理 レシピ
Q2 安田大サーカス 所属事務所 よゐこ 所属事務所 ますだおかだ 所属事務所
Q3 2011-12 シーズン 日本フットサルリーグ 参加クラブ数
Case4 Q1 吉野料理 材料 吉野料理 レシピ 吉野料理 特徴
Q2 よゐこ 所属事務所 安田大サーカス 所属事務所 ますだおかだ 所属事務所
Q3 2011-12 シーズン 日本フットサルリーグ 参加クラブ数 2011-12 シーズン Fリーグ 参加チーム数 2011-12 シーズン フットサルリーグ 日本 参加クラブ数
  • 定性評価を見てもわかるように、Case4が最もしっくりくる
  • Case2は「安田大サーカス」と「所属」「事務所」がそれぞれ別の検索ワードの候補として出されているのがよくない(出力のフォーマットの不一致)
  • Case3は、人間ならこううつだろうなという感じの検索ワード
  • Case4は、人間でもかなり上のほうの検索ワードで、「吉野料理 特徴」や「2011-12 シーズン Fリーグ 参加チーム数」のように問題文中にない単語を補って検索しているのが良い(これはプロンプトでは明示していない)

ハイパーパラメータチューニング(効いたもの)

検索ワードの作成のプロンプトエンジニアリングが最も効いたが、それ以外のハイパーパラメータも効果はあった。ここでは効果があったもののみ示す

いずれも、数%~5%程度の上昇なので、検索ワード生成のプロンプトエンジニアリングのほうがはるかに差が出る。

RAGに入れるコンテクストを増やす(+)

  • 検索で10件取ってきて、類似度の上位度5件をソートして入れるよりも、上位度10件をソートして入れたほうが良かった。これは情報の取りこぼしが減るから
  • 精度の寄与度は数%程度
正誤判定者 Embedding 正解率(コンテクスト=5) 正解率(コンテクスト=10)
GPT-3.5 V3-small 49.4% 50.0%
GPT-3.5 V3-large 48.2% 50.4%
GPT-4 V3-small 62.6% 64.0%
GPT-4 V3-large 61.4% 63.4%
  • コンテクスト=5, =10は、RAG回答時に入れるGoogleの結果。10のほうが一貫して正解率が高くなる
  • Embedding APIによる差(text-embedding-3-small, text-embedding-3-large)はほとんどなかったので、以降はsmallとする
  • 検索ワード生成、RAGによる回答はGPT-3.5

検索回数を増やす(++)

  • 検索ワードは3個生成させているので、検索ワードは増やすことは可能
  • 検索回数は2回までは精度が5%程度上がるが、それ以降は打ち止めになる
  • Custom Search JSON APIのデイリーリミットが1万回で、これに引っかかりやすくなるので代償がある
検索回数 コンテクスト 正解率(正誤判定GPT-3.5) 正解率(正誤判定GPT-4)
1 10 50.2% 64.2%
2 10 55.6% 68.4%
2 20 55.4% 69.0%
3 10 55.8% 70.6%
3 20 54.4% 69.2%
3 30 55.4% 70.0%

検索ワード生成をGPT-4に行わせる(+)

  • 検索ワード生成の部分だけGPT-4に行わせる
  • 検索ワードの生成はそこまでトークン数が重くないので、GPT-4が安くなった今ならある程度釣り合いは取れる。
  • 特にGPT-3.5評価時に精度向上効果があり、精度寄与度は数%
検索ワード生成 正解率(正誤判定GPT-3.5) 正解率(正誤判定GPT-4)
GPT-3.5 50.0% 64.0%
GPT-4 53.6% 65.4%

うまくいかなかったもの

逆効果なプロンプトエンジニアリング(-)

  • Case1:「あなたは検索エンジンです」とシステムプロンプトに入れる
    • よくやられるハックであるが、このケースだと逆効果だった(数%程度のマイナス
  • Case2: 検索ワード生成プロンプトを英語にする
    • ChatGPTの言語能力は英語のほうが上なので、検索ワード生成の部分を英語にする
    • 一般にはこれは効くこともあるが、これも逆効果だった(数%程度のマイナス
    • 回答の部分も英語にしたらわからない。また、元文章やタスクが変わると効くかもしれない。今回はかなり日本のドメイン特有の問題なので、英語が活きる問題ではない
条件 正解率(正誤判定GPT-3.5) 正解率(正誤判定GPT-4)
ベースライン 50.0% 64.0%
「検索エンジンです」指定 46.4% 60.4%
検索ワード生成プロンプトを英語化 46.4% 61.4%

ベースラインはプロンプトエンジニアリングのCase4

ソーシャルメディアの情報を入れる(-)

  • Google検索では、ソーシャルメディアの情報(X)がたまに入っていることがある(pagemap -> socialmediaposting -> articlebody
  • この情報を入れたところ精度は変わらない~逆に落ちてしまった(数%のマイナス
  • 特にコンテクストサイズを増やして、SNS由来の情報がRAGに混入しやすいケースで落ちる
  • 問題のケースバイケースだろうが、SNSの情報はノイジーなことが多いので逆効果だったのではないか
SNS情報 コンテクスト 正解率(正誤判定GPT-3.5) 正解率(正誤判定GPT-4)
なし 10 50.0% 64.0%
あり 10 50.4% 63.8%
あり 20 47.6% 62.6%

プラスだった効果を全部混ぜる

今までプラスだった効果の以下を全部混ぜる

  • ベースラインのCase4
  • 検索ワードの生成をGPT-4にする
  • 検索を2回行い、コンテクストを20個にする
正解率(正誤判定GPT-3.5) 正解率(正誤判定GPT-4)
ベースライン 50.0% 64.0%
全増し 57.6% 71.2%

無事、きれいな結果になった。

正解率(正誤判定GPT-3.5) 正解率(正誤判定GPT-4) 差分
ベースライン 50.0% 64.0% 0.0% 0.0%
+(1)検索ワード生成をGPT-4にする 53.6% 65.4% 3.6% 1.4%
+(2)検索回数を2回にし、コンテクストを20にする 55.4% 69.0% 5.4% 5.0%
全増し((1)+(2)) 57.6% 71.2% 7.6% 7.2%

Custom Search JSON APIのday1万クォーターがかなり厳しいので、検索は1回でも良さそう。以前のBingチャットは2回検索していたが、最近は1回の検索になっている印象があるため、概ねBingチャットのやっていることは正解に近そう。

所感

  • 思ったよりもプロンプトに左右される
  • 普段はふわっとなりがちなプロンプトエンジニアリングも、下流タスクを決めてその正解率を見ることで(この場合はクイズ)、既存の機械学習の問題のように扱えることがわかった。逆にいうと、定量化できるタスクやデータセットを先に決めてしまうのが大事

コード

少量データの作成

import json
import numpy as np

def main():
    with open("dataset/abc_01-12_hard.json", "r", encoding="utf-8") as fp:
        data = json.load(fp)
    np.random.seed(912)
    np.random.shuffle(data)
    with open("dataset/abc_01-12_hard_500.json", "w", encoding="utf-8") as fp:
        json.dump(data[:500], fp, ensure_ascii=False, indent=4, separators=(',', ': '))

if __name__ == "__main__":
    main()

ベースラインのコード

from openai import OpenAI
import json
import os
import concurrent.futures
from tqdm import tqdm
import traceback
from googleapiclient.discovery import build
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

CREDENTIALS = {
    "API_KEY": "<your_api_key>",
    "CSE_ID": "<your_cse_id>",
}

ANSWERER_PROMPT = """あなたはクイズの回答者です。以下の正誤判定基準に留意して、ユーザーから与えられた問題と文脈に応じてに返答してください。返答は問題の回答のみ教えて下さい
回答の際は、文脈(Context)を使用して、ユーザー入力の最後の質問(Question)に答えてください。

## ユーザーの入力
Context:
{ここに文脈が書かれます}


Question: {ここに与えられた問題が書かれます}
Helpful Answer:

## 正誤判定基準
【人名について】
・人名に関しての敬称・肩書(「○○さん」「○○選手」など)の有無は、正誤判定には関係ない。しかし、明らかに間違った敬称・肩書をつけた場合は誤答扱いにする。
・人名は原則として、芸名・ペンネーム・登録名など最も一般的に知られている名前、及びそれに準ずる知名度のもののみを正解とし、本名や別号など「知名度が低い」と正誤判定者が判断した解答は誤答の対象とする。前者の正解対象が回答に含まれていれば正解とする。
・東洋人名(日本・韓国・中国・台湾など、主に氏名表記に漢字が用いられる国)は、フルネームでの解答に限り正解とする。ただし、東洋人名でも、通称が固有名詞化した場合や、四股名はこの限りではない。
・西洋人名は、原則ファミリーネームのみで正解とする。ただし、明らかにフルネームを要求される問題の場合は例外とする。
・複合姓はファミリーネーム同様、区別の必要性がある場合のみ求め、原則一般的な呼称で正解とする。
・以上の人名に関するルールは、実在しないキャラクター等にも適用する。

【複数解答について】
・複数のものから一つだけを答える問題の場合、問題文のままの順序で全てを答えた場合は正解とするが、それ以外の場合、全てを解答するのは不正解とする。
・都道府県名と都市名の両方を答えた場合、原則都市名の方を答えたものとみなす。

【文章での解答について】
・歌詞や文章の一部を問う問題で、問われているもの以上を解答した場合は、たとえ歌詞・文章通りでも誤答の対象とすることがある。
・ただし、ことわざや慣用句など、全体で1つのイディオムとして認知されている言葉はこの限りではない。
・ことわざや慣用句のすべてを答えた際、問われているものは正解だが、問題文と一部が異なっていた場合は誤答の対象とすることがある。"""

SEARCHER_PROMPT = """以下の条件に基づいて、ユーザーの質問に対して、その回答を効率よく見つけるために検索エンジンに与える検索ワードを抽出し生成してください。

・入力された質問から、主要なキーワードやテーマを識別し、それに関する適切な出力として検索ワードをまとめてください。
・各検索ワードは目的の答えや情報を効率的に見つけるための主要要素を含むべきです。
・質問の中で英語を指定してきても、検索ワードは日本語で提示してください。
・検索ワードの候補を3つ出してください。検索エンジンに与えたときに最も回答を見つける可能性の高い検索ワードを1番目に出してください。
・出力はJSON形式で、search_wordsをキーとして、値はリスト形式で出してください。"""

RAG_USER_PROMPT = """Context:
{context}


Question: {question}
Helpful Answer:"""

def run_search(item:dict, search_prompt_model_name:str):
    # Get search words
    try:
        client = OpenAI()    
        response = client.chat.completions.create(
            model=search_prompt_model_name,
            messages=[
                {"role": "system", "content": SEARCHER_PROMPT},
                {"role": "user", "content": item["original_question"]},
            ],
            temperature=0,
            response_format={ "type": "json_object" },
            max_tokens=512)

        item.update({"search_words": response.choices[0].message.content})
        usage = {
            "prompt_tokens": response.usage.prompt_tokens,
            "completion_tokens": response.usage.completion_tokens,
            "total_tokens": response.usage.total_tokens
        }
        item.update({"search_usage": usage})
    except Exception as ex:
        print(traceback.print_exc())
        return None

    search_words = json.loads(response.choices[0].message.content)["search_words"]

    # run google search
    try:
        service = build('customsearch', 'v1', developerKey=CREDENTIALS["API_KEY"])
        result = service.cse().list(q=search_words[0], cx=CREDENTIALS["CSE_ID"], num=10, lr="lang_ja").execute()
        item["search_result"] = result
        contexts = []
        for search_item in result["items"]:
            if "snippet" in search_item:
                contexts.append(f'{search_item["title"]}\n{search_item["snippet"]}')
        item["search_context"] = contexts
        return item
    except Exception as ex:
        print(traceback.print_exc())
        return None

def run_answer(item: dict, ask_model_name: str, embeddings_model: str, take_num: int=5):
    client = OpenAI()

    # run embeddings
    try:
        response = client.embeddings.create(
            input=item["search_context"],
            model=embeddings_model
        )
        embeddings_search = np.array([x.embedding for x in response.data]) # (N_query, dim)
        usage = {
            "prompt_tokens": response.usage.prompt_tokens,
            "total_tokens": response.usage.total_tokens
        }

        response = client.embeddings.create(
            input=item["original_question"],
            model=embeddings_model
        )
        usage = {
            "prompt_tokens": response.usage.prompt_tokens + usage["prompt_tokens"],
            "total_tokens": response.usage.total_tokens + usage["total_tokens"]
        }


        embeddings_question = np.array([x.embedding for x in response.data]) # (1, dim)
        score = cosine_similarity(embeddings_question, embeddings_search)[0]
        sort_indices = np.argsort(score)[::-1]

        item.update({"search_usage": usage})

        use_contexts = []
        for i in sort_indices[:take_num]:
            use_contexts.append(item["search_context"][i])
    except Exception as ex:
        print(traceback.print_exc())
        return None

    # run answer
    try:
        client = OpenAI()
        user_prompt = RAG_USER_PROMPT.format(context="\n".join(use_contexts), question=item["original_question"])
        # print(user_prompt)
        response = client.chat.completions.create(
            model=ask_model_name,
            messages=[
                {"role": "system", "content": ANSWERER_PROMPT},
                {"role": "user", "content": user_prompt},
            ],
            temperature=0,
            max_tokens=512)

        item.update({"final_answer": response.choices[0].message.content})
        usage = {
            "prompt_tokens": response.usage.prompt_tokens,
            "completion_tokens": response.usage.completion_tokens,
            "total_tokens": response.usage.total_tokens
        }
        item.update({"answer_usage": usage})
        return item
    except Exception as ex:
        print(traceback.print_exc())
        return None


def run_quiz(item: dict, search_prompt_model_name: str, 
             answer_model_name: str, embedding_model_name: str, take_num: int,
             cnt : int):
    item = run_search(item, search_prompt_model_name)
    item = run_answer(item, answer_model_name, embedding_model_name, take_num=take_num)

    output_path = f"raw_result/search_{search_prompt_model_name}/answer_{answer_model_name}/embedding_{embedding_model_name}/take_{take_num}/{cnt:04}.json"
    os.makedirs(os.path.dirname(output_path), exist_ok=True)
    with open(output_path, "w", encoding="utf-8") as fp:
        json.dump(item, fp, ensure_ascii=False, indent=4, separators=(',', ': '))
    print(item["final_answer"], item["original_question"])
    return item


def run_experiment(search_prompt_model_name: str, answer_model_name: str, embedding_model_name: str, take_num: int):
    with open("dataset/abc_01-12_hard_500.json", encoding="utf-8") as fp:
        json_data = json.load(fp)

    with concurrent.futures.ProcessPoolExecutor(max_workers=5) as executor:
        futures = []
        for cnt, item in enumerate(json_data):
            output_path = f"raw_result/search_{search_prompt_model_name}/answer_{answer_model_name}/embedding_{embedding_model_name}/take_{take_num}/{cnt:04}.json"
            if os.path.exists(output_path):
                continue
            futures.append(executor.submit(run_quiz, item, search_prompt_model_name, 
                                answer_model_name, embedding_model_name, take_num, cnt))

        for future in tqdm(concurrent.futures.as_completed(futures), total=len(futures)):
            item = future.result()

def main():
    search_prompt_model_names = ["gpt-3.5-turbo-1106"]
    answer_model_names = ["gpt-3.5-turbo-1106"]
    embedding_model_names = ["text-embedding-3-small", "text-embedding-3-large"]
    take_nums = [5, 10]

    for p1 in search_prompt_model_names:
        for p2 in answer_model_names:
            for p3 in embedding_model_names:
                for p4 in take_nums:
                    run_experiment(p1, p2, p3, p4)

if __name__ == '__main__':
    main()

正誤判定

from openai import OpenAI
import json
import os
import traceback
import concurrent.futures
from tqdm import tqdm
import glob

EVALUATOR_PROMPT = """あなたはクイズの正誤判定者です。回答者から与えられた「対象の回答」を、「問題文」と「問題の答え」を参考に、正誤判定してください。ユーザーの入力は以下のフォーマットで与えられます。正誤判定基準は以下にあるので、これを参考に正誤判定してください。

結果はJSONで、「is_correct」をキーとし、正解ならtrue, 不正解ならfalseを値として返してください。

## ユーザーの入力
・問題文
{ここに問題文が入力されます}

・対象の回答
{ここに回答者の回答が入力されます}

・問題の答え
{ここに問題の本当の答えが書かれます。この行または次の行に、表記揺れによる別の正解や、問題特有の正誤判定基準が付与される場合もあります}

## 正誤判定基準
【よみがなについて】
・回答や問題の答えには、漢字とそのよみがなが付与されることがある。例:織田信長(おだのぶなが)。漢字で答えた場合は、漢字で判定し、英語やひらがな・カタカナで答えた場合はよみがなをベースに、正誤判定すること。

【人名について】
・人名に関しての敬称・肩書(「○○さん」「○○選手」など)の有無は、正誤判定には関係ない。しかし、明らかに間違った敬称・肩書をつけた場合は誤答扱いにする。
・人名は原則として、芸名・ペンネーム・登録名など最も一般的に知られている名前、及びそれに準ずる知名度のもののみを正解とし、本名や別号など「知名度が低い」と正誤判定者が判断した解答は誤答の対象とする。前者の正解対象が回答に含まれていれば正解とする。
・東洋人名(日本・韓国・中国・台湾など、主に氏名表記に漢字が用いられる国)は、フルネームでの解答に限り正解とする。ただし、東洋人名でも、通称が固有名詞化した場合や、四股名はこの限りではない。
・西洋人名は、原則ファミリーネームのみで正解とする。ただし、明らかにフルネームを要求される問題の場合は例外とする。
・複合姓はファミリーネーム同様、区別の必要性がある場合のみ求め、原則一般的な呼称で正解とする。
・以上の人名に関するルールは、実在しないキャラクター等にも適用する。

【複数解答について】
・複数のものから一つだけを答える問題の場合、問題文のままの順序で全てを答えた場合は正解とするが、それ以外の場合、全てを解答するのは不正解とする。
・都道府県名と都市名の両方を答えた場合、原則都市名の方を答えたものとみなす。

【文章での解答について】
・歌詞や文章の一部を問う問題で、問われているもの以上を解答した場合は、たとえ歌詞・文章通りでも誤答の対象とすることがある。
・ただし、ことわざや慣用句など、全体で1つのイディオムとして認知されている言葉はこの限りではない。
・ことわざや慣用句のすべてを答えた際、問われているものは正解だが、問題文と一部が異なっていた場合は誤答の対象とすることがある。"""


def run_evaluation(item:dict, orig_filename:str, evaluation_model_name:str):
    try:
        client = OpenAI()
        additional_prompt = '\n'+item['original_additional_info'] if len(item['original_additional_info']) > 0 else ''
        eval_user_prompt = f"""・問題文
{item["original_question"]}

・対象の回答
{item["final_answer"]}

・問題の答え
{item["original_answer"]}{additional_prompt}"""

        response = client.chat.completions.create(
            model=evaluation_model_name,
            messages=[
                {"role": "system", "content": EVALUATOR_PROMPT},
                {"role": "user", "content": eval_user_prompt},
            ],
            temperature=0,
            max_tokens=512,
            response_format={"type": "json_object"}
        )

        content = json.loads(response.choices[0].message.content)
        # print(eval_user_prompt, content)
        item.update({"evaluation": content["is_correct"]})
        item.update({"evaluation_model": evaluation_model_name})
        usage = {
            "prompt_tokens": response.usage.prompt_tokens,
            "completion_tokens": response.usage.completion_tokens,
            "total_tokens": response.usage.total_tokens
        }
        item.update({"usage_eval": usage})

        output_path = orig_filename.replace("raw_result", f"eval_{evaluation_model_name}")
        os.makedirs(os.path.dirname(output_path), exist_ok=True)
        with open(output_path, "w", encoding="utf-8") as f:
            json.dump(item, f, ensure_ascii=False, indent=4, separators=(",", ": "))        
        return item
    except Exception as ex:
        print(item, orig_filename)
        print(traceback.print_exc())
        return None

def main(evaluation_model_name: str):
    answerer_files = sorted(glob.glob(f"raw_result/**/*.json", recursive=True))

    if "gpt-3.5" in evaluation_model_name:
        max_concurrency = 20
    else:
        max_concurrency = 10
    with concurrent.futures.ThreadPoolExecutor(max_workers=max_concurrency) as executor:
        futures = []
        for cnt, ans_file in enumerate(answerer_files):
            output_path = ans_file.replace("raw_result", f"eval_{evaluation_model_name}")
            # print(ans_file)
            if os.path.exists(output_path):
                continue
            with open(ans_file, "r", encoding="utf-8") as fp:
                item = json.load(fp)

            futures.append(executor.submit(run_evaluation, item, ans_file, evaluation_model_name))

        for future in tqdm(concurrent.futures.as_completed(futures), total=len(futures)):
            future.result()

if __name__ == '__main__':
    main(evaluation_model_name="gpt-3.5-turbo-1106")
    main(evaluation_model_name="gpt-4-0125-preview")


Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です