HyDE (Hypothetical Document Embeddings: 仮説的なドキュメントの埋め込み)とは、通常の Embedding による文書検索よりも高い精度での検索を実現する vector index の検索手法です。HyDE というのは、質問そのものを vector として検索を行うよりも、質問に対する仮の解(LLMによる解)を Embedding に使った方が、検索の精度が高くなるという仮説に基づきます。
参考:https://langchain.readthedocs.io/en/latest/modules/indexes/examples/hyde.html
元論文:https://arxiv.org/abs/2212.10496
LLM による文書作成は、 ChatGPT の流行開始時によく揶揄されたように、人間にとってももっともらしく聞こえる「嘘、 AI による捏造」が含まれる場合があります。しかし一方で、信頼できるソースからの情報検索に使用できる情報が含まれている、とされています。
HyDE では、この LLM による仮のドキュメントを埋め込み、それを元に信頼できるソース、つまり自分たちが作成したドキュメント、マニュアル、ナレッジベースの検索を行います。
よく分からない単語を人間が検索して、「正解は見つけられなかったけど関連する用語が分かったからそれを元に更に検索しよう」というような感じでしょうか。
LangChain ではこの HyDE 仮説を簡単に実行可能なラッパーが用意されているので、それを利用して vector 検索と QA の作成をやってみます。
LangChain の HyDE をやってみる(GPT-3.5-turboで)
公式サンプルは GPT-3 だけど、お値段高いですからね…
いろいろimport
# OpenAI APIs
from langchain.chat_models import ChatOpenAI
from langchain.embeddings import OpenAIEmbeddings
# LLMChain, HyDE
from langchain.chains import LLMChain, HypotheticalDocumentEmbedder
# カスタムテンプレート用(Chat特化)
from langchain.prompts import PromptTemplate
from langchain.prompts.chat import (
# メッセージテンプレート
ChatPromptTemplate,
# System メッセージテンプレート
SystemMessagePromptTemplate,
# assistant メッセージテンプレート
AIMessagePromptTemplate,
# user メッセージテンプレート
HumanMessagePromptTemplate,
)
from langchain.schema import (
# それぞれ GPT-3.5-turbo API の assistant, user, system role に対応
AIMessage,
HumanMessage,
SystemMessage
)
# env に読み込ませるAPIキーの類
import key
# 環境変数にAPIキーを設定
import os
os.environ["OPENAI_API_KEY"] = key.OPEN_API_KEY
基本的には、 GPT-3.5-turbo を使う基本セットと、Embedding を行う基本セットがあれば大丈夫です。実際に検索を行う際に、index を作成する DB 等は import します。
その他は、 prompt のカスタムのために LLMChain と、HyDE を実現する HypotheticalDocumentEmbedder を import します。
とりあえず埋め込みやってみる
# 基本的なオブジェクト生成
base_embeddings = OpenAIEmbeddings()
llm = ChatOpenAI()
# HyDE モジュールに同梱されている web_search prompt を使って HyDE を作る
embeddings = HypotheticalDocumentEmbedder.from_llm(llm, base_embeddings, "web_search")
# 埋め込み query
result = embeddings.embed_query("Where is the Taj Mahal?")
print(result)
#[-0.008099346421658993, 0.003965758252888918, ...... ,-0.028891606256365776]
基本的には、通常の Embedding と大差はないです。ただし、使用する LLM を引数に指定する点が異なります。 “web_search” という引数は、使用するPrompt Template で、いくつか(元論文にあるものが?)標準で同梱されています。今回は、多分基本的な “web_search” を使っています。
query を embed する方法は、通常の embedder と変わりませんね。返り値を見て見ると、 vector が得られているのが分かります。
複数の LLM 文書を使って見る
# 複数の埋め込みを結合(精度が上がる?)
multi_llm = ChatOpenAI(n=4, best_of=4)
embeddings = HypotheticalDocumentEmbedder.from_llm(llm, base_embeddings, "web_search")
result = embeddings.embed_query("Where is the Taj Mahal?")
# print(result)
複数の LLM を与え、そのベクトルの平均を取ることで精度が上げられる……らしいです。複数使用する場合は、 llm のコンストラクタに 使用したい数を渡してインスタンスを作成します。以降の手順は、全く同様となります。
独自の Prompt を使う
LangChain では通常のLLM の操作と同様に、 LLMChain を利用して HyDE に与える独自の Prompt の作成を容易に行えます。LangChain によると、Query が使われるドメイン(多分、関心の領域とか、分野とか)が事前に分かっている場合、それと関連の深い Prompt を与えることにより、検索の精度が上げられるとのことです。
ということで、せっかくなので ChatOpenAI 用の Prompt でやってみます(面倒なので普通の prompt でもいい気がします)。
# 日本語で回答するように独自prompt を使用する LLMChainを作成
prompt_template = "Please answer questions from users in Japanese like a management consultant."
user_template = """Question: {question}
Answer:"""
system_message_prompt = SystemMessagePromptTemplate.from_template(prompt_template)
human_message_prompt = HumanMessagePromptTemplate.from_template(user_template)
chat_prompt = ChatPromptTemplate.from_messages([system_message_prompt, human_message_prompt])
chat_prompt.input_variables = ["question"]
# カスタムプロンプトを使ったLLMChain から HyDE を生成
llm_chain = LLMChain(llm=llm, prompt=chat_prompt)
embeddings = HypotheticalDocumentEmbedder(llm_chain=llm_chain, base_embeddings=base_embeddings)
# 日本語でembedding してみる
result = embeddings.embed_query("経営戦略の重要な判断基準を3つ教えてください。")
Prompt を日本語にするかどうか? は結構悩みどころな気がしますが、ここでは日本語の回答を要求するだけに止めています。実際のプロダクトに組み込む場合は、様々な prompt をテストして性能を評価した方がよさそうです。
Vector Search と QA の実行
準備
# Vector 格納 / FAISS
from langchain.vectorstores import FAISS
# テキストファイルを読み込む
from langchain.document_loaders import TextLoader
from langchain.text_splitter import CharacterTextSplitter
loader = TextLoader('reiwa4-1.txt')
documents = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=500, chunk_overlap=0, separator="\n")
docs = text_splitter.split_documents(documents)
必要なモジュールのimport を行い、テキストドキュメントを読み込んでいます。 chunk をもっと小さく、separator を「。」などにしてより細かく分割した方が、精度の評価はしやすいかもしれません。
# HyDE の embeddings を使って vector indexを作成
db = FAISS.from_documents(docs, embeddings)
query = "A社の主力商品は?"
docs = db.similarity_search(query)
print(docs[0].page_content)
from langchain.chains.question_answering import load_qa_chain
chain = load_qa_chain(ChatOpenAI(), chain_type="stuff")
print("----------------")
print(chain({"input_documents": docs, "question": query}, return_only_outputs=True))
FAISS を使って index を生成する際に、HyDE で作成した embeddings を使用しています。
以降の検索、QA の実行については、通常の vector search と一緒です。
結果
ここ数年、A 社では、直営店や食品加工の分野に展開を行っている。これらの業務は、常務が中心となって 5 名の生産に従事する若手従業員と 5 名のパート従業員が兼任の形で従事している。A 社は、2010 年代半ばに自社工場を設置するとともに、地元の農協と契約し倉庫を借りることになった。自社工場では、外部取引先からパン生地を調達し、自社栽培の新鮮で旬の野菜(トマトやレタスなど)やフルーツを使ったサンドイッチや総菜商品などを製造し、既存の大手中食業者を含めた複数の業者に卸している。作り手や栽培方法が見える化された商品は、食の安全志向の高まりもあり人気を博している。
—————-
{‘output_text’: ‘A社の主力商品は自社栽培の新鮮で旬の野菜やフルーツを使用したサンドイッチや総菜商品で、作り手や栽培方法が見える化されているため、食の安全志向の高まりもあり人気を博しています。また、地元菓子メーカーとの共同開発である、サツマイモを使った洋菓子も人気商品となっています。’}
元になったドキュメントが小さいので、精度としては通常の Embedding と比較するのは難しいですね……(試してみましたが、全く同じ箇所が抜き出されました)。とはいえ、間違っているわけでもなく、「人気」と「主力」がいい感じに理解されていると思います。もっと多様なドキュメントを解釈するときに、実験してみるといいでしょう。
まとめ
- HyDE は LLM の出力した仮の文書を元に vector を作成する
- 小さいドキュメントだと差が分からない
- 事前に適用領域が分かっている場合は、備え付けの Prompt ではなく、自分たちでの prompt の調整が必要
- 大きめのドキュメントがないと実験しても評価が難しい
- Chat用Promptは zero-shot な検索の時などに面倒臭い