前回(https://qiita.com/mebi/items/1f29335a7afb9e92c440 )、生成AIを使った検索でRAGの実装しましてみました。
今回は、RAGを改良したHyDEという手法をためして見たいと思います。
HyDEの仕組み
RAGは、
予め検索対象ドキュメントを生成AIを通しておき、Embeddingというベクトル情報を作成しておきます。
そして、ユーザー入力に対して、Embeddingを作成します。
2つのEmbeddingが近しい(差が0に近い)ドキュメントを検索結果として、そのドキュメントをもとにユーザー質問へ答える・・・というものでした。
ここで、ユーザーは「質問」しているのにRAG側では「回答」を探しており、
そもそも「質問」と「回答」は意味的に近しいのか?という疑問が生まれます。
そしてそのあたりは、生成AIの元データだったり学習のさせ方に依存んしていたり、実質ブラックボックスの状態なので知るすべがありません。
HyDEではこの疑問に答えます。
HyDEは、ユーザーからの「質問」に対して、一度「架空の、質問の答えとなるテキスト、架空の回答」を生成します。
そして、その架空の回答に対してEmbeddingを算出するのです。
以降はRAGの流れとなります。
「質問」と「質問に対する回答」よりも、
「質問に対する架空の回答」と「質問に対する(真の)回答」のほうがマッチしやすそうなのが体感でわかります。
(テキストの文章構造などを想像してみてください)
実装
前回(https://qiita.com/mebi/items/1f29335a7afb9e92c440)
の実装に加えて、HyDEを追加実装します。
まずは、架空のドキュメントを作成する部分です。
# 要件に合わせて具体的に書いたほうが良さそう。
# 例えば、「架空の社内規定を作って」など。
def make_hypothetical_document_prompt(input_text):
prompt = "次のユーザー質問への回答や対応が書いてある、架空の文書の内容を出力してください。\n"
prompt += "ユーザーが、ルールを尋ねているのか、具体的な対応を尋ねているのかに注視して、回答文書の内容を記述してください\n"
prompt += "・適切に改行を入れてください\n"
prompt += "# ユーザーの質問 \n\n"
prompt += input_text + "\n\n"
prompt += "# あなたの答え"
return prompt
def make_hypothetical_document(input_text, api_key):
prompt = make_hypothetical_document_prompt(input_text)
openai.api_key = api_key
response = openai.ChatCompletion.create(
model="gpt-4o",
messages=[
{"role": "user",
"content": prompt},
],
)
ret_msg = response.choices[0]["message"]["content"].strip()
return ret_msg
あとはユーザー入力からEmbeddingを作成する処理を
架空回答文書から作成するように差し替えればOK
INPUT_TEXT="ユーザーからの入力を設定します"
# RAG版の実装は以下のコメントアウト。
# input_vector = get_embedding(INPUT_TEXT)
# dists = calc_ebd_dist(df, input_vector)
# HyDE版の実装はこちら。
hy_doc = make_hypothetical_document(INPUT_TEXT, API_KEY)
input_vector = get_embedding(hy_doc)
dists = calc_ebd_dist(df, input_vector)
# 以降は同じ〜。
実装を終えて。
仕組みが簡単で、非常に明快な改良法なので良かったです。
デメリットはAPI呼び出し回数が2倍になることです。
各種要件と相談ですね(gpt-4oがでて、コスト面で制約がゆるくなったので やりやすくはなっています)
Embeddingの出力を見る。
あと、最後にRAG版とHyDE版で、
入力Embedding と 検索ドキュメントのEmbeddingの差を出力してみました。
差 = 2ベクトル間の距離で 0 〜 1.0 です。
そして、これおは何度か入力質問を変えて、
「RAG版が誤った結果を返す、HyDE版が正しい結果を返す」
結果となったときのEmbeddingの差になります。
(これは、うまく性能差を引き出す質問ができずRAGもHyDEも、期待するドキュメントを参照してばっかりいたからです。結果に違いがある質問をするのに苦労しました・・・)
ファイル名 | RAG版Embeddingとの差 | HyDE版Embeddingとの差 |
---|---|---|
リファレンス1.txt(HyDE版が参照したドキュメント) | 0.594 | 0.503 |
リファレンス2.txt | 0.601 | 0.512 |
リファレンス3.txt | 0.632 | 0.579 |
... | ... | ... |
リファレンス98.txt | 0.619 | 0.560 |
リファレンス99.txt(RAG版が参照したドキュメント) | 0.571 | 0.530 |
0に近いほど、よいリファレンスドキュメント検索ができたと言えます。
今回の質問では、リファレンス1.txtを参照してもらいたかったのですが、
RAG版では他のドキュメントがヒットしていました。
RAGからHyDEにすることで、
全体としてEmbeddingの差が0.04〜0.05ほど改善していますが、
より正解に近いドキュメントでは、0.09〜0.10ほど改善しておりまして、
HyDEにすることで、より正解に近いドキュメントのほうが、Embeddingの差がより小さくなる傾向が確認できました。