見出し画像

LlamaIndex の 評価機能 を試す

「LlamaIndex」の「評価」の機能を試したのでまとめました。

・LlamaIndex 0.10.26


1. LlamaIndexの評価

RAGアプリケーションの性能を向上させるには、その性能を「評価」する必要があります。

・Retrieval Evaluation (取得評価)
ベクトルストアから取得するコンテキスト (チャンク) の品質を評価します。
具体的には、リトリーバーで期待するコンテキストを取得できるかどうかを測定します。

・Response Evaluation (応答評価)
クエリエンジンが生成する応答の品質を評価します。
具体的には、応答が取得したコンテキストの情報と一致するかどうか (幻覚がないかどうか) を測定します。

2. ドキュメントの準備

今回は、マンガペディアの「ぼっち・ざ・ろっく!」のドキュメントを用意しました。

・bocchi.txt

3. 質問応答

Google Colabでの質問応答の実行手順は、次のとおりです。

(1) パッケージのインストール。

# パッケージのインストール
!pip install llama-index==0.10.26

(2) 環境変数の準備。
左端の鍵アイコンで「OPENAI_API_KEY」を設定してからセルを実行してください。

# 環境変数の準備 (左端の鍵アイコンでOPENAI_API_KEYを設定)
import os
from google.colab import userdata
os.environ["OPENAI_API_KEY"] = userdata.get("OPENAI_API_KEY")

(3) ログレベルの設定。

import logging
import sys

# ログレベルの設定
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, force=True)

(4) Colabにdataフォルダを作成してドキュメントを配置。
左端のフォルダアイコンでファイル一覧を表示し、右クリック「新しいフォルダ」でdataフォルダを作成し、ドキュメントをドラッグ&ドロップします。

(5) ドキュメントの読み込み。

from llama_index.core import SimpleDirectoryReader

# ドキュメントの読み込み
documents = SimpleDirectoryReader("data").load_data()

(6) インデックスの作成。
「インデックス」は、データのクエリを可能にするデータ構造を保持するコンポーネントです。ドキュメントをチャンクに分割し、チャンク毎に埋め込みに変換して保持します。
チャンクは類似検索の対象となるデータ単位になります。

from llama_index.core import VectorStoreIndex

# インデックスの作成
index = VectorStoreIndex.from_documents(documents)

(7) クエリエンジンの作成。
「クエリエンジン」は、ユーザー入力(クエリ)と関連する情報をインデックスから取得し、それをもとに応答を生成するモジュールです。

# クエリエンジンの作成
query_engine = index.as_query_engine()

(8) 質問応答。

# 質問応答
print(query_engine.query("ぼっちちゃんの得意な楽器は?"))
後藤ひとりの得意な楽器はギターです。

4. 質問コンテキストデータセットの生成

「評価」に利用する「質問コンテキストデータセット」を生成します。「質問」と質問に回答するための情報が含まれる「コンテキスト」のペアのデータセットになります。

(1) 非同期処理のための前処理
Colabノートブックで非同期処理のコードを実行するための前処理を実行します。

# Colabで実行するための前処理
import nest_asyncio
nest_asyncio.apply()

(2) ドキュメントからノードを取得してノードIDを指定。
デフォルトのノードIDはランダムなUUIDになります。同じIDを確保するために、手動で連番を指定しています。

from llama_index.core.node_parser import SentenceSplitter

# ドキュメントからノードを取得
node_parser = SentenceSplitter()
nodes = node_parser.get_nodes_from_documents(documents)

# ノードIDを手動で設定
for idx, node in enumerate(nodes):
    node.id_ = f"node_{idx}"

(3) 質問コンテキストデータセットの生成。
日本語テンプレートも設定しています。これを設定しないと英語のデータセットが生成されました。

from llama_index.core.evaluation import generate_question_context_pairs
from llama_index.llms.openai import OpenAI

# テンプレート
DEFAULT_QA_GENERATE_PROMPT_TMPL = """コンテキスト情報は以下のとおりです。

---------------------
{context_str}
---------------------

予備知識がなく、文脈情報が与えられた場合。
以下のクエリに基づいて問題のみを生成します。

あなたは先生です。
あなたのタスクは、今後の試験用に {num_questions_per_chunk} 個の質問を作成することです。
問題は文書全体にわたって多様でなければなりません。
設問は提供されたコンテキスト情報に限定してください。"""

# 質問コンテキストデータセットの生成
qa_dataset = generate_question_context_pairs(
    nodes,
    llm=OpenAI(model="gpt-3.5-turbo"),
    num_questions_per_chunk=2,
    qa_generate_prompt_tmpl=DEFAULT_QA_GENERATE_PROMPT_TMPL,
)

(4) 質問コンテキストデータセットの保存と読み込み。 (オプション)

# 質問コンテキストデータセットの保存
qa_dataset.save_json("pg_eval_dataset.json")
from llama_index.core.evaluation import EmbeddingQAFinetuneDataset

# 質問コンテキストデータセットの読み込み
qa_dataset = EmbeddingQAFinetuneDataset.from_json("pg_eval_dataset.json")

(5) 生成したデータセットの確認 (先頭5つ)。

# 生成したデータセットの確認 (先頭5つ)
for key in list(qa_dataset.queries.keys())[:5]:   
    print("queries", qa_dataset.queries[key])
    print("relevant_docs", qa_dataset.relevant_docs[key])
queries 後藤ひとりがバンド活動を始めるきっかけとなった出来事は何ですか?
relevant_docs ['node_0']
queries ライブチケットのノルマをこなすためにひとりが行った行動について説明してください。
relevant_docs ['node_0']
queries 結束バンドが文化祭ライブで演奏することを決めた理由は何ですか?
relevant_docs ['node_1']
queries デモ審査のためにMVを作成する際、結束バンドはどのような困難に直面しましたか?
relevant_docs ['node_1']
queries 後藤ひとりはなぜギターを始めたのですか?その結果、どのような活動をしていますか?
relevant_docs ['node_2']

5. Retrieval Evaluation (取得評価)

(1) インデックスとリトリーバーの準備。
ノードIDを手動で指定したノードからインデックスを生成します。

# インデックスとリトリーバーの準備
vector_index = VectorStoreIndex(nodes)
retriever = vector_index.as_retriever(similarity_top_k=3)

(2) 「RetrieverEvaluator」の準備。

from llama_index.core.evaluation import RetrieverEvaluator

# RetrieverEvaluatorの準備
retriever_evaluator = RetrieverEvaluator.from_metric_names(
    ["mrr", "hit_rate"], 
    retriever=retriever
)

今回は、次の2つの評価指標を設定しました。

・Hit Rate
取得した上位k個のコンテキスト内に正しい答えが含まれている割合を計算します。含まれてる場合1、含まれてない場合0になります。

・MRR (Mean Reciprocal Rank)
各クエリの最上位にある関連ドキュメントのランクを調べることにより、システムの精度を評価するための指標としてランクの逆数を計算します。取得した上位k個のコンテキスト内の正しい答えのランクが1番の場合1、2番の場合0.5、3番の場合0.333…になります。

(3) 評価の実行。
「mmr」と「hit_rate」の各スコアの平均で評価します。スコアが1に近いほど、取得品質が高いことを意味します。

mrr_values = []
hit_rate_values = []

# 評価の実行
for key in list(qa_dataset.queries.keys()):
    # 1クエリの評価の実行
    result =retriever_evaluator.evaluate(
        query=qa_dataset.queries[key], 
        expected_ids=qa_dataset.relevant_docs[key]
    )

    # 集計
    mrr_values.append(result.metric_dict["mrr"].score)
    hit_rate_values.append(result.metric_dict["hit_rate"].score)

    # 確認
    print(result)

# 確認
mmr_average = sum(mrr_values) / len(mrr_values)
hit_rate_average = sum(hit_rate_values) / len(hit_rate_values)
print("MRR:", mmr_average, "Hit Rate:", hit_rate_average)
Query: 後藤ひとりがバンド活動を始めるきっかけとなった出来事は何ですか?
Metrics: {'mrr': 1.0, 'hit_rate': 1.0}
Query: ライブチケットのノルマをこなすためにひとりが行った行動について説明してください。
Metrics: {'mrr': 0.3333333333333333, 'hit_rate': 1.0}
    :
MRR: 0.6611111111111112 Hit Rate: 0.8333333333333334

6. Response Evaluation (応答評価)

(1) 「FaithfulnessEvaluator」の準備。
日本語テンプレートも設定しています。

from llama_index.core.prompts import PromptTemplate
from llama_index.core.evaluation import FaithfulnessEvaluator
from llama_index.llms.openai import OpenAI

DEFAULT_EVAL_TEMPLATE = PromptTemplate(
    """特定の情報がコンテキストによってサポートされているかどうかを教えてください。
YES か NO で答える必要があります。
コンテキストの大部分が無関係であっても、コンテキストのいずれかが情報をサポートしている場合は、YES と答えてください。いくつかの例を以下に示します。

Information: アップルパイは通常、二重の生地です。
Context: アップルパイは、主な詰め物の材料がリンゴであるフルーツパイです。
アップルパイには、ホイップクリーム、アイスクリーム、カスタード、チェダーチーズが添えられることがよくあります。
通常は二重の生地で、中身の上下にペストリーが入っています。上部の生地は固体または格子状の場合があります。
Answer: YES

Information: アップルパイは不味い。
Context: アップルパイは、主な詰め物の材料がリンゴであるフルーツパイです。
アップルパイには、ホイップクリーム、アイスクリーム、カスタード、チェダーチーズが添えられることがよくあります。
通常は二重の生地で、中身の上下にペストリーが入っています。上部の生地は固体または格子状の場合があります。
Answer: NO

Information: {query_str}
Context: {context_str}
Answer: """
)

# FaithfulnessEvaluatorの準備
evaluator = FaithfulnessEvaluator(
    llm=OpenAI(model="gpt-4-turbo-preview"),
    eval_template=DEFAULT_EVAL_TEMPLATE
)

(2) 評価の実行。
スコアが1に近いほど、応答がコンテキストと一致している (幻覚がない) ことがわかります。

faithfulness_values = []

# 評価の実行
for key in list(qa_dataset.queries.keys()):   
    # 1クエリの評価の実行
    response = query_engine.query(qa_dataset.queries[key])
    result = evaluator.evaluate_response(response=response)

    # 集計
    faithfulness_values.append(1 if result.passing else 0)

    # 確認
    print("Information:", qa_dataset.queries[key])
    print("Context:")
    for node in response.source_nodes:
        print(node.get_text())
    print("Answer:", result.passing)

# 確認
faithfulness_average = sum(faithfulness_values) / len(faithfulness_values)
print("Faithfulness:", faithfulness_average)
Information: 後藤ひとりは友達を作ることができない理由は何ですか?
Context:
結束バンド
後藤ひとりは友達を作れない陰キャでいつも一人で過ごしていたが、...
Answer: True
Information: 中学時代にテレビのインタビューを見て、後藤ひとりがバンドを組むことになったきっかけは何ですか?
Context:
結束バンド
後藤ひとりは友達を作れない陰キャでいつも一人で過ごしていたが、...
Answer: True    
    :
Faithfulness: 1.0



いいなと思ったら応援しよう!