LlamaIndex v0.10の「Putting It All Together」のQ&Aをやってみる+PandasQueryEngine

2024/02/26

こちらの公式ドキュメント(v0.10.12)を参考にQ&Aについてまとめていきます.

はじめに

ここまで,LlamaIndexを用いてデータをロードしインデックスを作成してクエリする一連の流れについて学んできました.

ここからは,本番環境を見据えた具体的なフレームワークに落とし込むための方法についてみていきます.

Putting It All Together

LLMを用いたアプリケーションは大きく3つに分けられます.

  • Q&A

  • チャットボット

  • エージェント構築

それぞれに特徴がありますが,Q&Aは一問一答,チャットボットは会話履歴をコンテキストとして考慮,エージェントは外部モジュール等のツールのアロケータとして機能,といった違いがあります.

今回は,その中でもQ&Aについて深堀していきます.


Q&Aパターン

本来は「Q&Aパターン」がh1見出しになるのですが,構成の都合上h2で代用します.

セマンティック検索

LlamaIndexの最も基本的な使用例は,セマンティック検索によるものです.すぐに開始するには,シンプルなインメモリのベクターストアを使用できますが,ベクターストアまとめの中からいずれかを選択して使用することもできます.

from llama_index.core import VectorStoreIndex, SimpleDirectoryReader

documents = SimpleDirectoryReader("data").load_data()
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
response = query_engine.query("What did the author do growing up?")
print(response)

このシンプルなコードは以前実装しています.

基本的な使用パターンを紹介している,もとの記事はこちらです.

要約

要約を求めるクエリでは,LLMが答えを合成するために,少なくないドキュメントを反復処理する必要があります.たとえば,次のような要約クエリがあります.

  • このテキスト集の要約は何?

  • X氏の会社での経験を要約してください.

  • カローラスポーツはどんな車ですか?

一般に,個の使用例には要約インデックス(SummaryIndex)が適しています.デフォルトでは,SummaryIndexはすべてのデータを対象にします.

経験的には,設定`response_mode="tree_summarize`によってもよい要約結果が得られるとされています.

index = SummaryIndex.from_documents(documents)

query_engine = index.as_query_engine(response_mode="tree_summarize")
response = query_engine.query("<summarization_query>")

このコードの実装はこちら

私が試した際は,あまり顕著な違いは見られなかったため,要約が主な使用目的である場合はSummaryIndex を使用することを検討しましょう.

詳細はこちら

VectorStoreIndexとSummaryIndexを比較した実装コードも参考にしてください.

構造化データに対するクエリ

LlamaIndexは,Pandas DataFrameであっても,SQL Databaseであっても,構造化データに対するクエリをサポートしています.

関連する文献を紹介します.

この中から,特にPandasを用いた表データの読み込みについて詳しくまとめます.

Pandas Query Engine

ここでは,`PandasQueryEngine` を使用して,自然言語をPandas Pythonコードに変換する方法を説明します.

つまり,日本語で命令を入力すると,そのデータをとってこれるようなPandasのPythonコードを生成し,その結果を出力してくれるという流れです.

`PandasQueryEngine` への入力は,Pandasデータフレームで,出力はレスポンスです.LLMは,結果を取得するために実行するデータフレーム操作を推測します.

注: PandasQueryEngine には、安全性を強化し、任意のコードの実行を防ぐための対策が講じられています。たとえば、private/dunder メソッドは実行されず、制限されたグローバルのセットにアクセスできません。

ここからは,実際のサンプルコードの解説をしていきます.

データセットのダウンロード

!pip install llama-index
import logging
import sys
from IPython.display import Markdown, display

import pandas as pd
from llama_index.core.query_engine import PandasQueryEngine


logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))

Toy DataFrame から始めましょう

都市と人口のペアを含むかなり単純なデータフレームをロードし,その`PandasQueryEngine`上で実行します.

`verbose=True` を設定することで,中間的に生成された命令を確認できます.

# Test on some sample data
df = pd.DataFrame(
    {
        "city": ["Toronto", "Tokyo", "Berlin"],
        "population": [2930000, 13960000, 3645000],
    }
)
query_engine = PandasQueryEngine(df=df, verbose=True)
response = query_engine.query(
    "What is the city with the highest population?",
)
INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
> Pandas Instructions:
```
df['city'][df['population'].idxmax()]
```
> Pandas Output: Tokyo
display(Markdown(f"<b>{response}</b>"))
## 東京
# get pandas python instructions
print(response.metadata["pandas_instruction_str"])
df['city'][df['population'].idxmax()]

LLM を使用して応答を合成するステップを実行することもできます。

query_engine = PandasQueryEngine(df=df, verbose=True, synthesize_response=True)
response = query_engine.query(
    "What is the city with the highest population? Give both the city and population",
)
print(str(response))
INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
> Pandas Instructions:
```
df.loc[df['population'].idxmax()]
```
> Pandas Output: city             Tokyo
population    13960000
Name: 1, dtype: object
INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
The city with the highest population is Tokyo, with a population of 13,960,000.

自作データセット

私も簡易なデータセットで実行してみました.

ダミーのデータセットは次のようにしました.

サンプルデータ

次に,3つのタイプのクエリを行いました.

クエリ1

IDをもとに情報を検索

クエリ2-1

締め切り日が同じ製品のID

たんてきにはこたえてくれませんが,締切日ごとに製品IDをまとめてくれました.

クエリ2-2

締切日がおなじ製品のIDのみを出力してくれました.

クエリ3

締切日が特定の月の製品

こちらは全く問題なし

クエリ3+


締切日の範囲で製品を検索

こちらもうまくいきました.言語の理解とコードに落とし込む力が十分に備わっていました.

さらにデータが増えたとき,データの形式が独特な場合でもたいおうできるかがカギになりそうです.

タイタニック号のデータセットの分析

ここでは,表を読み込んで分析します.元データはcsvを読み込みます.

!wget 'https://raw.githubusercontent.com/jerryjliu/llama_index/main/docs/examples/data/csv/titanic_train.csv' -O 'titanic_train.csv'

データを落としたら,読むこむために必要なのはこの2行です.後は同様です.

df = pd.read_csv("./titanic_train.csv")
query_engine = PandasQueryEngine(df=df, verbose=True)

以下は省略として,詳しくはコードサンプルを参照してください.

プロンプトの更新や命令もカスタマイズができるようです.

new_prompt = PromptTemplate(
    """\
You are working with a pandas dataframe in Python.
The name of the dataframe is `df`.
This is the result of `print(df.head())`:
{df_str}

Follow these instructions:
{instruction_str}
Query: {query_str}

Expression: """
)

query_engine.update_prompts({"pandas_prompt": new_prompt})

命令文字列はinstruction_str初期化時に渡すことでカスタマイズできます.

instruction_str = """\
1. Convert the query to executable Python code using Pandas.
2. The final line of code should be a Python expression that can be called with the `eval()` function.
3. The code should represent a solution to the query.
4. PRINT ONLY THE EXPRESSION.
5. Do not quote the expression.
"""

異種データ上のルーティング

LlamaIndexは,`RouterQueryEngine`を使って,異種データソース上でのルーティングもサポートしています.例えば,クエリを基礎となるドキュメントやサブインデックスに「ルーティング」したい場合などです.

これを行うには,まず,さまざまなデータソースに対してサブインデックスを構築します.次に,対応するクエリエンジンを構築し,各クエリエンジンに`QueryEngineTool`を取得するための説明を与えます.

from llama_index.core import TreeIndex, VectorStoreIndex
from llama_index.core.tools import QueryEngineTool

...

# define sub-indices
index1 = VectorStoreIndex.from_documents(notion_docs)
index2 = VectorStoreIndex.from_documents(slack_docs)

# define query engines and tools
tool1 = QueryEngineTool.from_defaults(
    query_engine=index1.as_query_engine(),
    description="Use this query engine to do...",
)
tool2 = QueryEngineTool.from_defaults(
    query_engine=index2.as_query_engine(),
    description="Use this query engine for something else...",
)

次に,それらのの上に`RouterQueryEngine`を定義します.デフォルトでは,`LLMSingleSelector`ルーターとして使用します.これは,LLMを使用して,説明を考慮してクエリをルーティングする最適なサブインデックスを選択します.

from llama_index.core.query_engine import RouterQueryEngine

query_engine = RouterQueryEngine.from_defaults(
    query_engine_tools=[tool1, tool2]
)

response = query_engine.query(
    "In Notion, give me a summary of the product roadmap."
)

ガイドはこちらです.

比較・対照クエリ

ComposableGraph内のクエリ変換モジュールを使用して,比較・コントラストクエリを明示的に実行できます.

from llama_index.core.query.query_transform.base import DecomposeQueryTransform

decompose_transform = DecomposeQueryTransform(
    service_context.llm, verbose=True
)

このモジュールは,既存のインデックス構造上で複雑なクエリをより単純なクエリに分割するのに役立ちます.

複数ドキュメントのクエリ

上記の明示的な合成・ルーティングフローに加えて、LlamaIndexはより一般的なマルチドキュメントクエリもサポートできます。これは、`SubQuestionQueryEngine`クラスを通じて行うことができます。
クエリが与えられると、このクエリエンジンは、最終的な回答を合成する前に、サブドキュメントに対するサブクエリを含む「クエリプラン」を生成します。

これを行うには、まず各ドキュメント・データソースのインデックスを定義し、それを`QueryEngineTool`で囲みます。

from llama_index.core.tools import QueryEngineTool, ToolMetadata

query_enginee_tools = [
    QueryEngineTool(
        query_engine=sept_engine,
        metadata=ToolMetadata(
            name="sept_22",
            description="Provides information about Uber quarterly financials ending September 2022",
        ),
    ),
    QueryEngineTool(
        query_engine=june_engine,
        metadata=ToolMetadata(
            name="june_22",
            description="Provides information about Uber quarterly financials ending June 2022",
        ),
    ),
    QueryEngineTool(
        query_engine=march_engine,
        metadata=ToolMetadata(
            name="march_22",
            description="Provides information about Uber quarterly financials ending March 2022",
        ),
    ),
]

次に、これらのツールに対して`SubQuestionQueryEngine`を定義します。

from llama_index.core.query_engine import SubQuestionQueryEngine

query_engine = SubQuestionQueryEngine.from_defaults(
    query_engine_tools=query_engine_tools
)

このクエリエンジンは、最終的な回答を合成する前に、クエリエンジンツールの任意のサブセットに対して任意の数のサブクエリを実行できます。このため、特定のドキュメントに関するクエリだけでなく、ドキュメント間の比較や対照のクエリにも特に適しています。

複数ステップのクエリ

LlamaIndexは、反復的な複数ステップのクエリもサポートできます。複雑なクエリが与えられた場合、それを最初のサブ質問に分割し、最終的な回答が返されるまで、返された回答に基づいてサブ質問を順番に生成します。

ガイド

時間的クエリ

LlamaIndexは、時間の理解が必要なクエリもサポートしています。これは次の2つの方法で実行できます。

  • 質問に答えるための追加のコンテキストを取得するために、クエリでノード間の時間的な関係(前、次の関係)を利用する必要があるかどうかを決定します。

  • 最新の順に並べ替え、古いコンテキストをフィルタリングします。

追加のリソース

まとめ

今回は「Putting It All Together」のQ&Aタスクについてまとめました。

  • Q&Aではセマンティック検索が最も用いられる

  • 要約タスクではSumaryIndexが利用できる

  • 構造化データに対するクエリもサポートしている

  • 複数ステップクエリや時間的クエリなど様々なクエリ法がある

次に読む記事

個人的には、複数ステップにクエリを分割しステップバイステップに解決できる

を見ていく予定です。


支援のお願い

ここまで読んでいただきありがとうございます。「スキ」で反応をいただけると励みになります。

また、継続的な記事の公開のために、支援をしていただけると幸いです。


この記事が気に入ったらサポートをしてみませんか?