高度なRAG 01:小から大への検索

『高度なRAG 01:小さなものから大きなものまでの探索』

子-親再帰リトリーバーとセンテンスウィンドウリトリーバー

RAG(Retrieval-Augmented Generation)システムは、与えられた知識ベースから関連情報を取得し、事実に基づいた文脈的に関連性のあるドメイン固有の情報を生成することができます。しかし、RAGは関連情報を効果的に取得し、高品質な応答を生成する際に多くの課題に直面しています。このシリーズのブログ記事/動画では、RAGのワークフローを最適化し、素朴なRAGシステムの課題に対処するための高度なRAGテクニックを解説します。

最初のテクニックは「small-to-bigリトリーバル」と呼ばれます。基本的なRAGパイプラインでは、リトリーバル用に大きなテキストチャンクを埋め込み、合成のためにも同じテキストチャンクを使用します。しかし、時には大きなテキストチャンクを埋め込み/リトリーブすることは最適でない場合もあります。大きなテキストチャンクにはセマンティックな表現を隠すためのたくさんのフィラーテキストが含まれている場合があり、リトリーバルが悪化する原因となります。もし、より小さな、よりターゲットに設定されたチャンクに基づいて埋め込み/リトリーブができ、それでもLLMが応答を合成するのに十分な文脈を持つことができるとしたらどうでしょうか?具体的には、リトリーバル用のテキストチャンクと合成用のテキストチャンクを切り離すことが有利です。より小さなテキストチャンクを使用することで、リトリーバルの精度が向上し、より大きなテキストチャンクはより多くの文脈情報を提供します。small-to-bigリトリーバルのコンセプトは、リトリーバルプロセス中に小さなテキストチャンクを使用し、その後、取得されたテキストが属する大きなテキストチャンクを大きな言語モデルに提供することです。

主な2つのテクニックがあります:

  1. より小さな子チャンクから参照するより大きな親チャンク:まずは取得時により小さなチャンクを取得し、次に親のIDを参照してより大きなチャンクを返します。
  2. センテンスウィンドウリトリーバル:取得時に単一の文を取得し、その文の周りのテキストのウィンドウを返します。

このブログ記事では、これら2つの手法をLlamaIndexでの実装について解説します。なぜLangChainではなくLlamaIndexで行っているのかというと、LangChainではすでに高度なRAGに関する多くのリソースが存在しているからです。重複努力は避けたいと考えています。また、私はLangChainとLlamaIndexの両方を使用しています。より多くのツールを理解し、柔軟に使用することが最善です。

このノートブックで全てのコードを見つけることができます。

基本的なRAGのレビュー

まず、4つの簡単なステップで基本的なRAGの実装を始めましょう:

Step 1. ドキュメントの読み込み

PDFReaderを使用してPDFファイルを読み込み、ドキュメントの各ページを1つのドキュメントオブジェクトに結合します。

loader = PDFReader()docs0 = loader.load_data(file=Path("llama2.pdf"))doc_text = "\n\n".join([d.get_content() for d in docs0])docs = [Document(text=doc_text)]

Step 2. ドキュメントをテキストチャンク(ノード)にパースする

次に、ドキュメントをテキストチャンクに分割します。LlamaIndexではこれらのテキストチャンクを「ノード」と呼びます。チャンクサイズは1024と定義しています。デフォルトのノードIDはランダムなテキスト文字列ですが、特定の形式に従うようにノードIDをフォーマットすることができます。

node_parser = SimpleNodeParser.from_defaults(chunk_size=1024)base_nodes = node_parser.get_nodes_from_documents(docs)for idx, node in enumerate(base_nodes):node.id_ = f"node-{idx}"

Step 3. 埋め込みモデルとLLMの選択

2つのモデルを定義する必要があります:

  • 埋め込みモデルは、各テキストチャンクに対してベクトル埋め込みを作成するために使用されます。ここではHugging FaceのFlagEmbeddingモデルを呼び出しています。
  • LLM:ユーザーのクエリと関連するテキストチャンクをLLMに入力し、関連コンテキストで回答を生成するために使用されます。

これら2つのモデルをServiceContextにまとめて、後のインデックス作成とクエリステップで使用することができます。

embed_model = resolve_embed_model(“local:BAAI/bge-small-en”)llm = OpenAI(model="gpt-3.5-turbo")service_context = ServiceContext.from_defaults(llm=llm, embed_model=embed_model)

ステップ4. インデックス、リトリーバー、およびクエリエンジンの作成

インデックス、リトリーバー、およびクエリエンジンは、データやドキュメントに関する質問を行うための3つの基本的なコンポーネントです:

  • インデックスは、外部ドキュメントからユーザークエリの関連情報を迅速に取得するためのデータ構造です。ベクトルストアインデックスは、テキストのチャンク/ノードを取り、それぞれのノードのテキストのベクトル埋め込みを作成し、LLMによるクエリを準備します。
base_index = VectorStoreIndex(base_nodes, service_context=service_context)
  • リトリーバーは、ユーザークエリに基づいて関連情報を取得し、取り出すために使用されます。
base_retriever = base_index.as_retriever(similarity_top_k=2)
  • クエリエンジンは、インデックスとリトリーバーの上に構築され、データに関する質問を行うための汎用インターフェースを提供します。
query_engine_base = RetrieverQueryEngine.from_args(    base_retriever, service_context=service_context)response = query_engine_base.query(    "安全微調整のキーコンセプトについて教えてください")print(str(response))

高度なメソッド1:より小さな子チャンクが大きな親チャンクを参照している

前のセクションでは、リトリーバルと合成の両方に固定のチャンクサイズ1024を使用しました。このセクションでは、リトリーバルにはより小さな子チャンクを使用し、合成にはより大きな親チャンクを参照する方法を探っていきます。最初のステップは、より小さな子チャンクを作成することです:

ステップ1:より小さな子チャンクの作成

チャンクサイズ1024のテキストチャンクごとに、さらに小さなテキストチャンクを作成します:

  • サイズ128のテキストチャンク8つ
  • サイズ256のテキストチャンク4つ
  • サイズ512のテキストチャンク2つ

元のサイズ1024のテキストチャンクをテキストチャンクのリストに追加します。

sub_chunk_sizes = [128, 256, 512]sub_node_parsers = [    SimpleNodeParser.from_defaults(chunk_size=c) for c in sub_chunk_sizes]all_nodes = []for base_node in base_nodes:    for n in sub_node_parsers:        sub_nodes = n.get_nodes_from_documents([base_node])        sub_inodes = [            IndexNode.from_text_node(sn, base_node.node_id) for sn in sub_nodes        ]        all_nodes.extend(sub_inodes)    # also add original node to node    original_node = IndexNode.from_text_node(base_node, base_node.node_id)    all_nodes.append(original_node)all_nodes_dict = {n.node_id: n for n in all_nodes}

すべてのテキストチャンク `all_nodes_dict` を見てみると、たとえば `node-0` には多くの小さなチャンクが関連付けられていることがわかります。実際、すべての小さなチャンクは、メタデータの中の大きなチャンクを指し示す `index_id` を持っています。

ステップ2:インデックス、リトリーバー、およびクエリエンジンの作成

  • インデックス:すべてのテキストチャンクのベクトル埋め込みを作成します。
vector_index_chunk = VectorStoreIndex(    all_nodes, service_context=service_context)
  • リトリーバー:ここでのポイントは、RecursiveRetriever を使用してノードの関係をトラバースし、“参照”に基づいてノードを取得することです。このリトリーバーは、ノードから他のリトリーバーやクエリエンジンへのリンクを再帰的に探索します。取得されたノードのうち、IndexNode のいずれかがあれば、それに紐づくリトリーバーやクエリエンジンを探索します。
vector_retriever_chunk = vector_index_chunk.as_retriever(similarity_top_k=2)retriever_chunk = RecursiveRetriever(    "vector",    retriever_dict={"vector": vector_retriever_chunk},    node_dict=all_nodes_dict,    verbose=True,)

質問をし、最も関連性の高いテキストのチャンクを取得すると、ノードIDが親チャンクを指すテキストチャンクを実際に取得し、親チャンクを取得します。

  • 前と同じ手順で、データに関する質問をするための一般的なインターフェースとして、クエリエンジンを作成することができます。
query_engine_chunk = RetrieverQueryEngine.from_args(    retriever_chunk, service_context=service_context)response = query_engine_chunk.query(    "安全な微調整のためのキーコンセプトについて教えてください")print(str(response))

高度なメソッド 2: 文のウィンドウ検索

さらに細かい検索を実現するために、小さな子チャンクではなく、ドキュメントを1つの文ごとに解析することができます。

この場合、単一の文はメソッド 1 で述べた「子」チャンクの概念に似ています。元の文の周辺(元の文の5文の前後)は「親」チャンクの概念に似ています。つまり、検索時には単一の文を使用し、検索された文と文のウィンドウを LLM に渡します。

ステップ 1: 文のウィンドウノードパーサーの作成

# create the sentence window node parser w/ default settingsnode_parser = SentenceWindowNodeParser.from_defaults(    window_size=3,    window_metadata_key="window",    original_text_metadata_key="original_text",)sentence_nodes = node_parser.get_nodes_from_documents(docs)sentence_index = VectorStoreIndex(sentence_nodes, service_context=service_context)

ステップ 2: クエリエンジンの作成

クエリエンジンを作成する際に、MetadataReplacementPostProcessor を使用して文を文のウィンドウに置き換えることができます。これにより、文のウィンドウが LLM に送られます。

query_engine = sentence_index.as_query_engine(    similarity_top_k=2,    # the target key defaults to `window` to match the node_parser's default    node_postprocessors=[        MetadataReplacementPostProcessor(target_metadata_key="window")    ],)window_response = query_engine.query(    "安全な微調整のためのキーコンセプトについて教えてください")print(window_response)

文のウィンドウ検索は、「安全な微調整のためのキーコンセプトについて教えてください」という質問に答えることができました。

実際に取得した文と文のウィンドウが表示されています。これにより、より多くの文脈と詳細が提供されます。

結論

このブログでは、RAG を改善するために小さなチャンクから大きなチャンクまで検索を行う方法について詳しく説明しました。Child-Parent RecursiveRetriever と LlamaIndex を使用した Sentence Window 検索に焦点を当てました。今後のブログ投稿では、他のトリックやヒントに深く入ります。進んだ RAG テクニックへのエキサイティングな旅にご期待ください!

参考文献:

By Sophia Yang on November 4, 2023

Connect with me on LinkedIn, Twitter, and YouTube and join the DS/ML Book Club ❤️

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more