
【RAGの精度を改善する!】HyDEについて
RAGの精度を改善する手法として、
こちらの書籍に書かれていた内容が大変参考になりました。
この記事では、RAGの精度改善手法のなかでも、
HyDEという手法について、
少しアレンジを加えたうえで分かりやすく紹介していきたいと思います。
なお、続編として、RAG-Fusionという手法も紹介していきますので、
こちらもご覧ください。
HyDEについて
HyDE(Hypothetical Document Embeddings)は、ユーザーからの質問に基づいてLLMで仮の回答を生成し、その回答をもとにデータベースを検索する手法です。
ユーザーが入力する質問とデータベースに保存されている回答は、必ずしも類似しているとは限りません。
なにせ、「質問」と「回答」ですからね。
そのため、HyDEでは、まず質問をもとに「仮の回答」(Hypothetical Document)を作ります。
この仮の答えをもとに、埋め込み(Embedding)を生成し、それを使ってデータベース内の情報を検索するのがHyDEという仕組みになります。
以下では、超シンプルなRAGとHyDEを取り入れたRAGを実装し、
精度を比較してみたいと思います。
Googleコラボで実装します。
ここから実装します。
準備
Langchainのインストール〜バージョン固定にしています。
!pip install langchain-core==0.3.0 langchain-openai==0.2.0
!pip install langchain-community==0.3.0 Gitpython==3.1.43 langchain_chroma==0.1.4 pdfplumber==0.11.4
ライブラリーのインポート
import requests
from io import BytesIO
import pdfplumber
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_core.documents import Document
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_community.retrievers import BM25Retriever
from pydantic import BaseModel, Field
OpenAIのAPIキーの設定(任意でLangSmithのセットアップ)
import os
from google.colab import userdata
os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')
# LangSmithのセットアップ(任意)
os.environ['LANGCHAIN_TRACING_V2'] = 'true'
os.environ['LANGCHAIN_ENDPOINT'] = 'https://api.smith.langchain.com'
os.environ['LANGCHAIN_API_KEY'] = userdata.get('LANGCHAIN_API_KEY')
os.environ['LANGCHAIN_PROJECT'] = 'RAG'
LangSmithのセットアップは任意ですが、
LangChainを便利に利用できるので、
ぜひ、一度触ってみることをお勧めします。
具体的には、以下の機能を利用することができます。
・LLMの実行ログの収集
・LLMの出力結果のデータセット化
・登録したデータセットによるモデルの評価
利用の際には、以下のサイトから、
サインアップとAPIキー取得をしてください。
ベクトルデータベースの構築
この記事では、みずほFGの統合報告書・人的資本レポートのPDFファイルを使います。実務でも参考になるかなと思います。
書籍では、データとしてLangchainのgithubにアップされている複数のREADMEファイルを対象としていましたが、英文書だったり、READMEファイルだったりと、実務的に参考にしにくいなあと思いました。
PDFファイル(リンク)の準備
ネット上のPDFファイルリンクをリストにします。
ここでは、みずほ銀行の有価証券報告書と統合報告書を使っています。
pdf_file_urls = [
"https://www.mizuho-fg.co.jp/investors/financial/report/yuho_202403/pdf/fg_fy.pdf",
"https://www.mizuho-fg.co.jp/investors/financial/disclosure/pdf/data24d_all_browsing.pdf",
]
PDFファイルを読み込む関数
def load_pdfs_with_pdfplumber(urls):
documents = []
for url in urls:
response = requests.get(url)
response.raise_for_status()
pdf_file = BytesIO(response.content)
with pdfplumber.open(pdf_file) as pdf:
full_text = ""
for page in pdf.pages:
text = page.extract_text()
if text:
full_text += text + "\n"
documents.append({"content": full_text, "metadata": {"source": url}})
return documents
requests.get(url): URLに対してHTTP GETリクエストを送信し、PDFファイルのコンテンツを取得します。
response.raise_for_status(): HTTPリクエストが成功したか(ステータスコード200)をチェックします。
BytesIO: ダウンロードしたPDFファイルのバイナリデータをメモリ上のファイルオブジェクトとして扱います。物理的なファイルを保存せずにPDFを処理できます。
pdfplumber.open(pdf_file): pdfplumberを使って、メモリ上のPDFファイルを開きます。
PDFファイルのテキストを分割する関数
def split_text_into_documents(documents, chunk_size=300, chunk_overlap=50):
splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
add_start_index=True,
)
docs = []
for doc in documents:
doc_chunks = splitter.split_text(doc["content"])
for chunk in doc_chunks:
docs.append(Document(page_content=chunk, metadata=doc["metadata"]))
return docs
RecursiveCharacterTextSplitter: 長文のテキストをchunkという単位で分割します。ここではchunkのサイズを300としています。また、文脈が途切れないよう、前のchunkと一部オーバーラープ(サイズ50)するようにしています。
関数の実行
documents = load_pdfs_with_pdfplumber(pdf_file_urls)
chunks = split_text_into_documents(documents)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
db = Chroma.from_documents(chunks, embeddings)
まずは超シンプルなRAGの実装
prompt = ChatPromptTemplate.from_template('''\
以下の文脈だけを踏まえて質問に答えてください。
文脈:###
{context}
###
質問:{question}
''')
model = ChatOpenAI(model="gpt-4o-mini",temperature=0)
retriever = db.as_retriever()
chain = {
"question":RunnablePassthrough(),
"context":retriever,
} | prompt | model | StrOutputParser()
質問を「みずほにおいて、個人と組織の目標を達成するためのキャリア開発の重要性とは?」として、chainから回答を得ます。
query = "みずほにおいて、個人と組織の目標を達成するためのキャリア開発の重要性とは?"
chain.invoke(query)
質問に対する回答は以下の通りです。
みずほにおいて、個人と組織の目標を達成するためのキャリア開発は非常に重要です。人材はみずほの最も大事な財産とされており、キャリア実現は人材を磨くプロセスと考えられています。キャリア面談やキャリアコレクションを通じて、社員が自分自身を理解し、現在の自分と将来の目指す姿とのギャップに気づくことが促されます。このプロセスを通じて、個人は自らのキャリアを描き、成長することができ、組織としてもその成長を支援することで、双方の目標を実現することが可能になります。さらに、社員が自律的に成長できる環境を整えることが、組織全体の生産性向上や意欲的な挑戦を促進するために必要とされています。
HyDEを組み込んだRAGの実装
まず質問をもとに、仮の回答(Hypothetical Document)を作ります。
# HyDE(Hypothetical Document Embeddings)
hypothetical_prompt = ChatPromptTemplate.from_template("""\
次の質問に回答する一文を書いてください
質問:{question}
""")
hypothetical_chain = hypothetical_prompt | model | StrOutputParser()
hypothetical_Document = hypothetical_chain.invoke(query)
hypothetical_Document
HyDEでの仮の回答(Hypothetical Document)は次のとおりです。
みずほにおいて、個人と組織の目標を達成するためのキャリア開発は、社員のスキル向上とモチベーションを高めることで、組織全体の競争力を強化し、持続的な成長を実現するために不可欠です。
仮の回答(Hypothetical Document)を使ったRAGのChain
hyde_rag_chain = {
"question": RunnablePassthrough(),
"context":hypothetical_chain | retriever,
} | prompt | model | StrOutputParser()
answer = hyde_rag_chain.invoke(query)
answer
HyDEを使った最終的な回答は以下のとおりとなりました。
みずほにおいて、個人と組織の目標を達成するためのキャリア開発の重要性は、以下の点に集約されます。
1. 個性や強みの発揮: キャリア開発を通じて、社員はそれぞれの個性や強みを発揮しやすくなります。これにより、社員が自分らしく成長を実感し、組織全体の生産性向上に寄与します。
2. 自律的な成長: みずほは「自律的に成長できる職場」を目指しており、キャリア開発は社員が自らの成長を促進するための重要な手段となります。これにより、社員は日々いきいきと仕事に臨むことができます。
3. 企業の成長との一致: 個人の成長と企業の成長の方向性を一致させることで、みずほの価値創造力を高めることができます。キャリア開発は、社員の成長が企業の成長に直結することを実現するための基盤となります。
4. エンゲージメントの向上: キャリア開発を通じて、社員のエンゲージメントが向上し、インクルーシブな組織づくりにも寄与します。これにより、社員がより積極的に組織に貢献する意欲が高まります。
以上のように、みずほにおけるキャリア開発は、個人の成長を促進し、組織全体の目標達成に向けた重要な要素となっています。
どうでしょうか?
HyDEの方が精度が良さそうですね。
他にもいろいろ試してみましたが、本件のように質問が抽象的な場合は、HyDEは効きそうです。逆に、例えば、みずほFGのCEOは誰か?みたいな、かなり具体的な質問の場合は、HyDEは効かないようです。
HyDEは、検索精度を向上させるための非常に興味深い手法です。
「ユーザーの質問そのもの」ではなく、「質問から生成した仮の回答」を使うのが肝ですが、本質は、ユーザーの質問をLLMを使って、コンピューターが処理しやすい最適な質問に変換していることだと思います。
この考え方は、「仮の回答」でなくとも、例えば、「ユーザーの質問を、データベースの検索精度が向上するような質問に変換して」みたいに、いろいろ応用できそうですね。
いいなと思ったら応援しよう!
