CCCMKホールディングス TECH LABの Tech Blog

TECH LABのエンジニアが技術情報を発信しています

ブログタイトル

LLMと様々なデータを繋ぐ"LlamaIndex"の基本的な使い方を調べて使ってみました。

こんにちは、CCCMKホールディングスTECH LABの三浦です。

この前近所で買い物をしていた時にふと空を見上げたら、飛行機が飛んでいるのを見つけました。その後数分くらいでまた次の飛行機が通り過ぎていき、すぐに次の飛行機が通り過ぎていく・・・。飛行機ってけっこう頻繁に飛んでいるんだな、と感じました。

さて、今回はLarge Language Model(LLM)の活用に関して最近調べたことを書いてみます。前から気になっていたLlamaIndexについてです。

gpt-index.readthedocs.io

LlamaIndexはLLMと外部の様々なデータを繋げる機能を提供するプロジェクトです。llama-indexというPythonのライブラリを使って、大量のデータを簡単にLLMと連結し、データに従ったQ&Aの機能などを実装することが出来ます。

基本的な形式のデータであればllama-indexで十分対応しているのですが、Data Connectorsを使うとさらに色々なデータと連携出来ます。Data ConnectorsはLlamaHubというレポジトリを通じて公開されており、ダウンロードして利用することが出来ます。

llamahub.ai

llama-indexは以前このブログでも触れたlangchainというライブラリを使って実装されています。langchainでもLLMに一度に入力しきれないような大量のデータをLLMと連結することが出来ましたが、llama-indexはその機能をより強化した印象です。

今回はllama-indexをAzure OpenAI Serviceで利用できるChatGPTのモデル(gpt-3.5-turbo)で試してみました。

やってみたこと

このブログには自分が色々試したことを書き留めた備忘録的な記事がいくつかあります。そういった記事は後日自分でも見ることが多いのですが、これらの記事の内容をベースにした自分専用のQAチャットボットがあったらいいな、と思っていました。

そこで今回llama-indexの基本的な使い方を理解するため、自分が書いたブログの内容に答えてくれるチャットボットの機能を実装してみました。このブログに掲載した"Azure"に関する5つの記事をピックアップしています。

Azure OpenAI Serviceを使用する場合の注意点

Azure OpenAI Serviceの仕様はOpenAIのものといくつか異なる点があります。llama-indexでは外部データの埋め込み表現の作成のところでその違いが影響します。Azure OpenAI Serviceを使用する場合は埋め込み表現用のモデルOpenAIEmbeddingsを指定する必要があります。

詳細はLlamaIndexのドキュメントのこちらのページを参照しました。

gpt-index.readthedocs.io

インストール

llama-indexを使用するために必要なライブラリをpipでインストールします。自分の環境依存の問題かもしれませんが、APIを実行する際にResource Not Found関係のエラーが頻発し、対応方法を調べるのにかなり苦戦しました。結局ライブラリのバージョンが影響していたようで、llama-indexおよびlangchainのバージョンを明示してインストールするようにしました。

pip install openai html2text llama-index==0.6.0.alpha3 langchain==0.0.142

html2textはHTMLをテキストに変換する用途で使用します。

llama-indexを使用する大まかな手順

  1. 接続したいデータをDocumentオブジェクトのリストとしてロードする
  2. DocumentオブジェクトからIndexを生成する
  3. Indexに対してクエリを実行する

2のIndexは、LlamaIndexではデータを構造化したものを表し、いくつかの種類が存在します。その中で今回はVector Store Indexを使用しました。

クエリ処理の流れはIndexの種類にも依るのですが、大まかに次の様に理解しました。まず質問文に対して関連する情報をIndexから検索(Retrieve)します。そしてそれらの情報を合成し(ResponseSynthesize)、最終的な回答を得る、という流れです。

データの読み込みからクエリ実行までのコード

では実際にデータを読み込んでクエリを実行するまでの流れをコードと一緒に見ていきます。

モジュールのインポート

必要になる各モジュールのインポートです。主にllama-indexlangchainからインポートします。合わせてloggingを使ってログ出力の設定を行います。

from langchain.chat_models import ChatOpenAI
from langchain.embeddings import OpenAIEmbeddings
from llama_index import LangchainEmbedding
from llama_index.llm_predictor.chatgpt import LLMPredictor
from llama_index import (
    SimpleWebPageReader,
    GPTVectorStoreIndex,
    LLMPredictor,
    PromptHelper,
    ServiceContext,
)
import logging
import sys
import os

# Loggerの設定
logging.basicConfig(stream=sys.stdout, level=logging.INFO) 
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))

使用するモデルの設定

次は一連の処理で使用するモデルの設定を行います。この辺りに先ほど触れた、Azure OpenAI Serviceでllama-indexを使う場合の注意点が盛り込まれています。

llm = ChatOpenAI(
  engine="gpt-35-turbo",
  temperature=0
)
embedding = OpenAIEmbeddings(
  model="text-embedding-ada-002"
)

llm_predictor = LLMPredictor(llm)
llama_embed = LangchainEmbedding(
  embedding,
  # 以下のパラメータはAzure OpenAIを使う場合は必須。
  # InvalidRequestError: Too many inputs.The max number of inputs is 1.がindex作成時に出る。
  embed_batch_size=1 
)

データの読み込み

外部データを読み込んでllama-indexのDocumentのリストに加工します。

url_lists = [
  ...
]

documents = SimpleWebPageReader(html_to_text=True).load_data(url_lists)

Promptの設定

llama-indexのPrompt生成を制御する各種設定です。LlamaIndexのドキュメントは英語を扱う場合を想定して書かれており、この辺りの設定はそのまま利用すると日本語の場合では上手くいかないことがあります。特に利用するトークンの数は、英語よりも日本語の方が大きくなる傾向があります。

# max LLM token input size
max_input_size = 4096
# set number of output tokens
num_output = 256
# set maximum chunk overlap
max_chunk_overlap = 20

prompt_helper = PromptHelper(max_input_size, num_output, max_chunk_overlap)

ServiceContextの作成

llama-indexがIndexを作ったりクエリを実行する際に必要になる部品をまとめたServiceContextを作成します。

service_context = ServiceContext.from_defaults(
  llm_predictor=llm_predictor,
  embed_model=llama_embed,
  prompt_helper=prompt_helper
)

Indexの作成とディスクへの保存

それではLlamaIndexの重要なパーツであるIndexの作成をします。今回のVector Store Indexの作成は、Azure OpenAI ServiceのAPIを使用するため利用料が発生します。一度作ったIndexはディスクなどに保存しておくことで、以降はそれを読み込むことで都度作成することなくIndexを使用することが出来るようになります。

index = GPTVectorStoreIndex.from_documents(documents=documents, service_context=service_context)
index.storage_context.persist('path/to/dir')

Query Engineを作り、クエリを実行する

最後にQuery Engineを作り、質問文を入力してクエリを実行します。

query_engine = index.as_query_engine(service_context=service_context)
response = query_engine.query('Azure Machine Learningについて教えて下さい。')

返ってきた回答

先ほどのクエリを実行すると、以下のような回答が生成されました。

print(response.response)
Azure Machine Learningは、機械学習プロジェクトで発生する様々なタスクを推進し、管理するAzureのサービスであり、モデルの推論APIをエンドポイントとして管理することができます。また、モデルはコンテナの環境で動かすのですが、コンテナを作るために必要となる環境設定やコンテナを動かすためのコンピュータリソースもAzure Machine Learningを通じて用意することができます。そしてコンテナを動かす場所としてKubernetesクラスタを指定することができ、負荷に応じて自動的にリソース調整するオートスケーリングを利用することができます。AzureではKubernetesをKubernetes Serviceで利用出来ます。Python SDKを使ってAzure Machine Learningをプログラムから操作することができます。

おぉ、たしかにこういった内容の文章を自分で書いたのを覚えています!ちゃんとした回答が得られているようです!

さらにデータのどの部分をソースとしてこの回答が得られたのかを確認することが出来ます。

print(response.get_formatted_sources(length=4096))
> Source (Doc id: ...)アーキテクチャより...

**Azure Machine Learning**
は機械学習プロジェクトで発生する様々なタスクを推進し、管理するAzureのサービスです。モデルの推論APIをエンドポイントとして管理することが出来ます。また、モデルはコンテナの環境で動かすのですが、コンテナを作るために必要となる環境設定やコンテナを動かすためのコンピュータリソースもAzure
Machine
Learningを通じて用意することが出来ます。そしてコンテナを動かす場所としてKubernetesクラスタを指定することが出来、負荷に応じて自動的にリソース調整するオートスケーリングを利用することが出来ます。AzureではKubernetesを
...

Promptを英語から日本語に変更する

今回は上手く行ったのですが、時々日本語で質問しているにも関わらず、回答が英語で返ってくることがありました。この原因は、llama-indexでクエリを実行する際に内部で生成される質問文が英語で記述されていることにあります。

llama-indexでは今回の処理においてQA PromptとRefine Promptが続けて実行されるため、それぞれのPromptのテンプレートを日本語に書き換えればこの問題を解消することが出来ました。

ちなみにデフォルトのテンプレートは以下のように確認することが出来ます。

from llama_index.prompts.default_prompts import DEFAULT_TEXT_QA_PROMPT_TMPL
from llama_index.prompts.chat_prompts import CHAT_REFINE_PROMPT_TMPL_MSGS

print(DEFAULT_TEXT_QA_PROMPT_TMPL) # QA Promptのテンプレート
print(CHAT_REFINE_PROMPT_TMPL_MSGS[2].format(context_msg="{context_msg}")) # Refine Promptのテンプレート

テンプレートを日本語のものに変えるコードです。

from llama_index.prompts.chat_prompts import CHAT_REFINE_PROMPT
from llama_index.prompts.prompts import RefinePrompt
from llama_index import QuestionAnswerPrompt

from langchain.prompts.chat import (
  AIMessagePromptTemplate,
  ChatPromptTemplate,
  HumanMessagePromptTemplate
)

QA_PROMPT_TMPL = (
    "以下の情報を参照してください。 \n"
    "---------------------\n"
    "{context_str}"
    "\n---------------------\n"
    "この情報を使って、次の質問に回答してください。: {query_str}\n"
)

CHAT_REFINE_PROMPT_TMPL_MSGS = [
  HumanMessagePromptTemplate.from_template("{query_str}"),
  AIMessagePromptTemplate.from_template("{existing_answer}"),
  HumanMessagePromptTemplate.from_template(
    """
    以下の情報を参照してください。 \n"
    "---------------------\n"
    "{context_msg}"
    "\n---------------------\n"
    この情報が回答の改善に役立つようならこの情報を使って回答を改善してください。
    この情報が回答の改善に役立たなければ元の回答を日本語で返してください。
    """)
]

CHAT_REFINE_PROMPT_LC = ChatPromptTemplate.from_messages(CHAT_REFINE_PROMPT_TMPL_MSGS)

QA_PROMPT = QuestionAnswerPrompt(QA_PROMPT_TMPL)
CHAT_PROMPT = RefinePrompt.from_langchain_prompt(CHAT_REFINE_PROMPT_LC)

QA_PROMPT_TMPLではIndexで検索したテキストと質問文がそれぞれcontext_strquery_strにセットされます。CHAT_REFINE_PROMPT_TMPL_MSGSでは回答の改善に役立つと思われる情報がcontext_msgにセットされます。

テンプレートはQuery Engineを作る際に設定することが出来ます。

query_engine = index.as_query_engine(
  service_context=service_context,
  text_qa_template=QA_PROMPT,
  refine_template=CHAT_PROMPT
)

これで日本語の質問文でクエリが実行されます。

まとめ

ということで、今回はLLMと外部データを連結する機能に長けたllama-indexというライブラリを使ってみた話をご紹介しました。llama-indexのドキュメントを眺めていると色々なことが出来そうなのですが、どこから手をつけようか迷っていました。今回はひとまず基本形ではあるもののllama-indexを使った一連の流れを体験することが出来てよかったと思います!