LangChainとVertex AI PaLMを用いた大規模ドキュメントでのスケーラブルな質問応答
Published on
LLMを用いた大規模ドキュメントでの質問応答についての紹介
質問応答(QA)は、自然言語で人間が提出した質問に自動的に回答することを目指すキーナチュラルランゲージプロセッシングのタスクです。PaLMのような大規模言語モデル(LLM)は、印象的なQAの機能を示していますが、そのトークンの制限(通常数千トークン程度)内に適合できるコンテキストの量に制限があります。これは、多数ページにわたる大規模なドキュメントのQAにおいて課題となります。
この記事では、LangChainフレームワークとGoogleのVertex AI PaLM APIを組み合わせて、大規模ドキュメント用のスケーラブルなQAシステムを構築する方法について探求します。以下の方法について説明します。
- スタッフィング - ドキュメント全体をコンテキストとして押し込む
- マップリデュース - ドキュメントをチャンクに分割して並列処理を行う
- リファイン - ドキュメントのチャンクを反復的に洗練する
- 類似検索 - ベクトル埋め込みを使用して関連するチャンクを見つける
各アプローチの利点と制限について比較します。完全なコードはこのColabノートブックで利用できます。
サンプルドキュメント(50ページ)に対する各方法のメトリクスを比較しましょう。
方法 | 関連ドキュメント | LLM呼び出し | トータルトークン | 回答の品質 |
---|---|---|---|---|
スタッフィング | 3ページ | 1 | 8432 | 良好 |
マップリデュース | 50ページ | 51 | 63019 | まあまあ |
リファイン | 50ページ | 50 | 71209 | 良好 |
類似検索 | 4ページ | 5 | 5194 | すばらしい |
類似検索アプローチは、フルドキュメントの方法と比較して、10分の1のページ数、LLM呼び出し、およびトークンで高品質の回答を見つけることができます。この差は、より大規模なデータセットではさらに広がるでしょう。
Step 1. 大規模ドキュメントでの質問応答のためのLangChainのセットアップ
まず、Vertex AI SDK、LangChain、およびChromaDBを含む必要な依存関係をインストールします。
!pip install google-cloud-aiplatform langchain==0.0.323 chromadb==0.3.26 pypdf
主要なライブラリをインポートします。
from langchain.document_loaders import PyPDFLoader
from langchain.llms import VertexAI
from langchain.chains.question_answering import load_qa_chain
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import Chroma
from langchain.embeddings import VertexAIEmbeddings
PaLMテキストモデルと埋め込みモデルをロードします。
vertex_llm_text = VertexAI(model_name="text-bison@001")
vertex_embeddings = VertexAIEmbeddings(model_name="textembedding-gecko@001")
Step 2. ドキュメントのロード
この例では、MLOpsに関するPDFのホワイトペーパーを使用します。ダウンロードし、PyPDFLoaderを使用してテキストをロードします。
pdf_url = "https://services.google.com/fh/files/misc/practitioners_guide_to_mlops_whitepaper.pdf"
pdf_loader = PyPDFLoader(pdf_file)
pages = pdf_loader.load_and_split()
これにより、PDFがページに分割され、これをベースドキュメントとして使用できます。
Step 3. ドキュメントのスタッフィング
最もシンプルなアプローチは、フルドキュメントテキストをLLMのコンテキストウィンドウに押し込むことです。プロンプトテンプレートをセットアップします。
prompt_template = """提供されたコンテキストを使用して、できるだけ正確に質問に回答してください。
コンテキストに回答が含まれていない場合は、「コンテキストに回答はありません」と言ってください。\n\n
コンテキスト:\n{context}?\n
質問:\n{question}\n
回答:
"""
prompt = PromptTemplate(
template=prompt_template, input_variables=["context", "question"]
)
スタッフィングQAチェーンをロードします。
stuff_chain = load_qa_chain(vertex_llm_text, chain_type="stuff", prompt=prompt)
その後、質問を実行します。
question = "実験とは何ですか?"
context = "\n".join(str(p.page_content) for p in pages[:7])
stuff_answer = stuff_chain(
{"input_documents": pages[7:10], "question": question}, return_only_outputs=True
)
これは動作しますが、モデルが処理できるコンテキストサイズに制限があります(数千トークン程度)。フル50ページドキュメントの詰め込みでは、この制限に達します。
try:
print(stuff_chain(
{"input_documents": pages[7:], "question": question},
return_only_outputs=True))
except Exception as e:
print("The code failed since it won't be able to run inference on such a huge context")
Step 4. マップリデュース
より大規模なドキュメントにスケーリングするには、それらをチャンクに分割し、各チャンクでQAを実行し、その結果を集計することができます。LangChainは、これを処理するためのマップリデュースチェーンを提供します。
まず、質問と結果を組み合わせるプロンプトを定義します。
question_prompt_template = """
提供されたコンテキストを使用して、できるだけ正確に質問に回答してください。\n\n
コンテキスト:\n{context}\n
質問:\n{question}\n
回答:
"""
question_prompt = PromptTemplate(
template=question_prompt_template, input_variables=["context", "question"]
)
combine_prompt_template = """抽出されたコンテンツと質問を考慮して、最終的な回答を作成してください。
コンテキストに回答が含まれていない場合は、「コンテキストに回答はありません」と言ってください。\n\n
サマリー:\n{summaries}?\n
質問:\n{question}\n
回答:
"""
combine_prompt = PromptTemplate(
template=combine_prompt_template, input_variables=["summaries", "question"]
)
質問と結果のプロンプトを指定して、マップリデュースチェーンをロードします。
map_reduce_chain = load_qa_chain(
vertex_llm_text,
chain_type="map_reduce",
return_intermediate_steps=True,
question_prompt=question_prompt,
combine_prompt=combine_prompt,
)
フルドキュメントセットで実行してください:
map_reduce_outputs = map_reduce_chain({"input_documents": pages, "question": question})
これは、各ページに個別にQAを実行し、最終ステップで結果を組み合わせます。中間結果を検査することができます:
for doc, out in zip(
map_reduce_outputs["input_documents"], map_reduce_outputs["intermediate_steps"]
):
print(f"Page: {doc.metadata['page']}")
print(f"Answer: {out}")
マップリデュースアプローチは大きなドキュメントにスケーリングし、情報の取得元をいくつか示してくれる。しかし、最終の結合ステップで情報が失われることもある。
ステップ5. 磨き上げる
磨き上げるアプローチは、回答を反復的に洗練することで情報の喪失を軽減することを目的としています。最初のチャンクで初期回答を開始し、それぞれの後続チャンクで洗練された回答にしていきます。
既存の回答と新しいコンテキストを組み合わせた磨き上げるプロンプトを定義する。
refine_prompt_template = """
オリジナルの質問は:\n{question}\n\n
提供された回答は:\n{existing_answer}\n\n
次のコンテキストで必要に応じて既存の回答を洗練してください:\n{context_str}\n\n
取得したコンテンツと質問を考慮して、最終的な回答を作成してください。
コンテキストに回答が含まれていない場合は、"コンテキストに回答が利用できません"と答えてください。\n\n
"""
refine_prompt = PromptTemplate(
input_variables=["question", "existing_answer", "context_str"],
template=refine_prompt_template,
)
磨き上げるチェーンをロードする。
refine_chain = load_qa_chain(
vertex_llm_text,
chain_type="refine",
return_intermediate_steps=True,
question_prompt=initial_question_prompt,
refine_prompt=refine_prompt,
)
全体のドキュメントに対して実行する。
refine_outputs = refine_chain({"input_documents": pages, "question": question})
回答が洗練される様子を確認する。
for doc, out in zip(
refine_outputs["input_documents"], refine_outputs["intermediate_steps"]
):
print(f"Page: {doc.metadata['page']}")
print(f"Answer: {out}")
磨き上げるアプローチは、ドキュメント全体で情報を保持するのに役立ちますが、全体ドキュメントを直線的に処理する必要があります。
ステップ6. 類似度検索
効率化のために、埋め込みを使用して与えられた質問に対して最も関連のあるチャンクのみを検索できます。これにより、全文書を処理する必要がなくなります。
ChromaDBを使用して、ドキュメントのチャンクのベクトルインデックスを作成する。
vector_index = Chroma.from_documents(pages, vertex_embeddings).as_retriever()
質問に対して最も関連のあるチャンクを取得する。
docs = vector_index.get_relevant_documents(question)
これらの関連するチャンクだけでマップリデュースチェーンを実行する。
map_reduce_embeddings_outputs = map_reduce_chain(
{"input_documents": docs, "question": question}
)
print(map_reduce_embeddings_outputs["output_text"])
これにより、全文書の一部のみを処理することで、高品質な回答が得られます。類似度検索アプローチは、正確さと効率性のバランスが最も良い方法です。
結論
この記事では、LangChainとVertex AI PaLMを使用して、大規模なドキュメント上での質問応答に対するいくつかのアプローチを実証しました。小規模なドキュメントにはシンプルな stuffing が機能する場合もありますが、大量のデータにスケールするためには、マップリデュースと磨き上げるアプローチが必要です。
しかし、最も効率的かつ効果的な方法は、まずベクトルの類似検索を使用して、与えられた質問に対して最も関連のあるパッセージのみを検索することです。これにより、LLMが処理するテキストの量を最小限に抑えながら、高品質な回答が得られます。
類似度検索、LangChainのQAチェーン、そしてPaLMのような強力なLLMの組み合わせにより、大規模なドキュメントコレクションに対するスケーラブルな質問応答システムの構築が可能となります。このノートブックの全コードで始めることができます。
最新のLLMニュースを知りたいですか?最新のLLMリーダーボードをチェックしてください!