こんにちは、CCCMKホールディングスTECH LABの三浦です。
この前近所で買い物をしていた時にふと空を見上げたら、飛行機が飛んでいるのを見つけました。その後数分くらいでまた次の飛行機が通り過ぎていき、すぐに次の飛行機が通り過ぎていく・・・。飛行機ってけっこう頻繁に飛んでいるんだな、と感じました。
さて、今回はLarge Language Model(LLM)の活用に関して最近調べたことを書いてみます。前から気になっていたLlamaIndexについてです。
LlamaIndexはLLMと外部の様々なデータを繋げる機能を提供するプロジェクトです。llama-index
というPythonのライブラリを使って、大量のデータを簡単にLLMと連結し、データに従ったQ&Aの機能などを実装することが出来ます。
基本的な形式のデータであればllama-index
で十分対応しているのですが、Data Connectorsを使うとさらに色々なデータと連携出来ます。Data ConnectorsはLlamaHubというレポジトリを通じて公開されており、ダウンロードして利用することが出来ます。
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のドキュメントのこちらのページを参照しました。
インストール
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
を使用する大まかな手順
- 接続したいデータを
Document
オブジェクトのリストとしてロードする Document
オブジェクトからIndexを生成する- Indexに対してクエリを実行する
2のIndexは、LlamaIndexではデータを構造化したものを表し、いくつかの種類が存在します。その中で今回はVector Store Indexを使用しました。
クエリ処理の流れはIndexの種類にも依るのですが、大まかに次の様に理解しました。まず質問文に対して関連する情報をIndexから検索(Retrieve)します。そしてそれらの情報を合成し(ResponseSynthesize)、最終的な回答を得る、という流れです。
データの読み込みからクエリ実行までのコード
では実際にデータを読み込んでクエリを実行するまでの流れをコードと一緒に見ていきます。
モジュールのインポート
必要になる各モジュールのインポートです。主にllama-index
とlangchain
からインポートします。合わせて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_str
とquery_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
を使った一連の流れを体験することが出来てよかったと思います!