適切なチャンクサイズを評価する方法（LlamaIndexのブログをやってみた）

答えのない問い、それがチャンクサイズ。

LlamaIndexのブログに、評価によって適切なチャンクサイズを検討する方法があったので、試しました。

https://www.llamaindex.ai/blog/evaluating-the-ideal-chunk-size-for-a-rag-system-using-llamaindex-6207e5d3fec5

ブログ公開から半年以上経過しており、Duplicatedになっているライブラリーがあったので最新化して実行しました。

## 環境

Python: 3.11.9

llama-index-core: 0.10.55
llama-index-llms-bedrock: 0.1.12
llama-index-embeddings-bedrock: 0.2.1
llama-index-readers-file: 0.1.30
spacy: 3.7.5

RAGの対象ドキュメントは[AWS Well-Architected Framework](https://docs.aws.amazon.com/ja_jp/wellarchitected/latest/framework/welcome.html)のPDFとしました。

## 手順

まず、必要なライブラリーをインポートします。

```python
import time

from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from llama_index.core.evaluation import FaithfulnessEvaluator, RelevancyEvaluator
from llama_index.core.llama_dataset.generator import RagDatasetGenerator
from llama_index.core.prompts import PromptTemplate
from llama_index.core.settings import Settings
from llama_index.embeddings.bedrock import BedrockEmbedding
from llama_index.llms.bedrock import Bedrock
```

Claude 3 HaikuとClaude 3 SonnetのLLM、Titan Embedding V2を定義します。Settingsはデフォルト値をセットする感じ？と思います。

```python
llm_haiku = Bedrock(model="anthropic.claude-3-haiku-20240307-v1:0", temperature=0)
llm_sonnet = Bedrock(model="anthropic.claude-3-sonnet-20240229-v1:0", temperature=0)

embedding = BedrockEmbedding(model_name="amazon.titan-embed-text-v2:0")

Settings.llm = llm_haiku
Settings.embed_model = embedding
```

PDFを読み込みます。

```python
documents = SimpleDirectoryReader("./data/").load_data()
```

documentsはこんな感じです。PDFの1ページが1ドキュメントになっているようです。

```text:documents[5]
[Document(id_='ea125237-dfbd-486c-99cb-5c5f3a0b5998', embedding=None, metadata={'page_label': '1', 'file_name': 'wellarchitected-framework.pdf', 'file_path': '/workspaces/python3.11/0717-llamaindex/data/wellarchitected-framework.pdf', 'file_type': 'application/pdf', 'file_size': 10395957, 'creation_date': '2024-07-17', 'last_modified_date': '2024-07-12'}, excluded_embed_metadata_keys=['file_name', 'file_type', 'file_size', 'creation_date', 'last_modified_date', 'last_accessed_date'], excluded_llm_metadata_keys=['file_name', 'file_type', 'file_size', 'creation_date', 'last_modified_date', 'last_accessed_date'], relationships={}, text='AWS Well-Architected Framework ***Unable to locate subtitle***\nAWS Well-Architected Framework\n公開日: 2024 年 6 月 27 日  (改訂履歴 )\nAWS Well-Architected フレームワークは、AWS でシステムを構築する際に行う決定の長所と短所を\n理解するのに役立ちます。このフレームワークを使用することによって、信頼性が高く、安全で、効\n率的で、費用対効果が高く、持続可能なシステムを設計して運用するための、アーキテクチャに関す\nるベストプラクティスを学ぶことができます。\nはじめに\nAWS Well-Architected フレームワークは、AWS でシステムを構築する際に行う決定の長所と短所を\n理解するのに役立ちます。このフレームワークを利用すると、安全で信頼性が高く、効率的で、費用\n対効果が高く、持続可能なワークロードを AWS クラウド で設計および運用するための、アーキテ\nクチャに関するベストプラクティスを学ぶことができます。フレームワークにより、アーキテクチャ\nをベストプラクティスに照らし合わせて一貫的に測定し、改善すべき点を特定する手段を提供しま\nす。アーキテクチャのレビュープロセスは、アーキテクチャに関する決定についての前向きな話し合\nいであって、監査過程ではありません。当社では、Well-Architected システムを持つことが、ビジネ\nスで成功を収める可能性を大幅に向上させると考えています。\nAWS ソリューションアーキテクトは、さまざまな業種やユースケースに対応したソリューションの\nアーキテクトとして長年の経験を持っています。これまで何千ものお客様の AWS でのアーキテク\nチャの設計とレビューをお手伝いしてきました。その経験に基づいて、クラウド対応システムを設計\nするための核となる戦略とベストプラクティスを確立しました。\nAWS Well-Architected フレームワークは、特定のアーキテクチャがクラウドのベストプラクティス\nと整合しているかどうかを理解するための一連の基本的な質問を文書化したものです。このフレー\nムワークは、現代のクラウドベースのシステムに期待する品質を評価するための一貫したアプローチ\nと、その品質を達成するために必要な対応を提供します。AWS が進化し続け、お客様との共同作業\nからより多くを学び続ける中で、Well-Architected (よくできたアーキテクチャ) の定義に磨きをかけ\nていきます。\nこのフレームワークは、最高技術責任者 (CTO)、設計者、デベロッパー、オペレーションチームメ\nンバーなどの技術担当者を対象としています。本書では、クラウドワークロードを設計、運用する\n際に使用する AWS のベストプラクティスや戦略について説明し、さらなる実装の詳細やアーキテク\nチャパターンへのリンクも提供しています。詳細については、 AWS Well-Architected ホームページ \nへ.\nはじめに 1', mimetype='text/plain', start_char_idx=None, end_char_idx=None, text_template='{metadata_str}\n\n{content}', metadata_template='{key}: {value}', metadata_seperator='\n'),
```

ドキュメントを一部抜粋します。

```python
eval_documents = documents[5:15]
```

ドキュメントから質問を生成させます。

:::note
`RagDatasetGenerator.from_documents`で質問を生成できるのですが、標準状態では、英語で質問を生成する傾向があったので、プロンプトを少しだけカスタマイズしています。
:::

```python
eval_documents = documents[5:15]
data_generator = RagDatasetGenerator.from_documents(
    eval_documents,
    llm=llm_haiku,
    text_question_template=PromptTemplate(
        "**必ず日本語で回答してください。「はい、わかりました。以下の3つの質問を用意しました。」などの不要な出力は決して行わないでください。** Context information is below.\n---------------------\n{context_str}\n---------------------\nGiven the context information and not prior knowledge.\ngenerate only questions based on the below query.\n{query_str}\n"
    ),
    text_qa_template=PromptTemplate(
        "**必ず日本語で回答してください。「はい、わかりました。以下の3つの質問を用意しました。」などの不要な出力は決して行わないでください。** Context information is below.\n---------------------\n{context_str}\n---------------------\nGiven the context information and not prior knowledge, answer the query.\nQuery: {query_str}\nAnswer: "
    ),
)

eval_questions = data_generator.generate_questions_from_nodes()
```

生成した質問はこのようなものです。

```text:eval_questions.examples（一例）
AWS Well-Architected フレームワークの目的は何ですか?
AWS Well-Architected フレームワークを利用することで、どのようなメリットが得られますか?
このフレームワークの対象者は誰ですか?
AWS Well-Architected ツールの役割と提供する機能について説明してください。
```

なかなか良さそうに思います。


忠実度（Faithfulness）と関連性（Relevancy）を評価するEvaluatorを生成します。

```python
faithfulness = FaithfulnessEvaluator(llm=llm_sonnet)
relevancy = RelevancyEvaluator(llm=llm_sonnet)
```

評価する関数を定義します。

```python
# 指定されたチャンクサイズの平均応答時間、平均忠実度、平均関連性のメトリックを計算する関数を定義
# 応答を生成するために Claude 3 Haiku を使用し、応答を評価するために Claude 3 Sonnet を使用します。
def evaluate_response_time_and_accuracy(chunk_size, eval_questions):
    """
    Evaluate the average response time, faithfulness, and relevancy of responses generated by GPT-3.5-turbo for a given chunk size.

    Parameters:
    chunk_size (int): The size of data chunks being processed.

    Returns:
    tuple: A tuple containing the average response time, faithfulness, and relevancy metrics.
    """

    total_response_time = 0
    total_faithfulness = 0
    total_relevancy = 0

    # vector indexを作成
    Settings.chunk_size = chunk_size
    vector_index = VectorStoreIndex.from_documents(eval_documents)
    # query engineをビルド
    query_engine = vector_index.as_query_engine()
    num_questions = len(eval_questions.examples)

    # eval_questions 内の各質問を反復処理してメトリックを計算
    # BatchEvalRunnerはより高速な評価に使用できます (see: https://docs.llamaindex.ai/en/latest/examples/evaluation/batch_eval.html),
    # ここではループを使用して、さまざまなチャンク サイズの応答時間を具体的に測定します。
    for question in eval_questions.examples:
        start_time = time.time()
        response_vector = query_engine.query(question.query)
        elapsed_time = time.time() - start_time

        faithfulness_result = faithfulness.evaluate_response(
            response=response_vector
        ).passing

        relevancy_result = relevancy.evaluate_response(
            query=question.query, response=response_vector
        ).passing

        total_response_time += elapsed_time
        total_faithfulness += faithfulness_result
        total_relevancy += relevancy_result

    average_response_time = total_response_time / num_questions
    average_faithfulness = total_faithfulness / num_questions
    average_relevancy = total_relevancy / num_questions

    return average_response_time, average_faithfulness, average_relevancy
```

実行します。

```python
# さまざまなチャンク サイズを反復処理してメトリックを評価し、チャンク サイズを修正します。
for chunk_size in [128, 256, 512, 1024, 2048]:
    avg_time, avg_faithfulness, avg_relevancy = evaluate_response_time_and_accuracy(
        chunk_size, eval_questions
    )
    print(
        f"Chunk size {chunk_size} - Average Response time: {avg_time:.2f}s, Average Faithfulness: {avg_faithfulness:.2f}, Average Relevancy: {avg_relevancy:.2f}"
    )
```

## 結果

チャンクサイズは「128」または「256」の場合が、最も精度が良いという結果となりました。意外と（？）小さめチャンクが良さそうですね。

```
Chunk size 128 - Average Response time: 3.28s, Average Faithfulness: 1.00, Average Relevancy: 1.00
Chunk size 256 - Average Response time: 3.17s, Average Faithfulness: 1.00, Average Relevancy: 1.00
Chunk size 512 - Average Response time: 3.64s, Average Faithfulness: 0.96, Average Relevancy: 0.98
Chunk size 1024 - Average Response time: 4.12s, Average Faithfulness: 0.89, Average Relevancy: 1.00
Chunk size 2048 - Average Response time: 4.56s, Average Faithfulness: 0.82, Average Relevancy: 0.98
```

今回試したドキュメントが、たまたま小さいチャンクが適していた可能性がありますが、評価方法がわかったので、実際にRAGを構築する際には、同じ手順でまず良さそうなチャンクサイズを検討できますね。